diff --git a/.gitignore b/.gitignore index 660a296..3eb0e70 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,4 @@ Carthage/Build fastlane/report.xml fastlane/Preview.html fastlane/screenshots -fastlane/test_output - -Pods +fastlane/test_output \ No newline at end of file diff --git a/Doughnut/AppDelegate.swift b/Doughnut/AppDelegate.swift index 1bb09e2..da231af 100644 --- a/Doughnut/AppDelegate.swift +++ b/Doughnut/AppDelegate.swift @@ -1,10 +1,20 @@ -// -// AppDelegate.swift -// Doughnut -// -// Created by Chris Dyer on 22/09/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa import MASPreferences diff --git a/Doughnut/DoughnutApp.swift b/Doughnut/DoughnutApp.swift index 9f634b6..54a1655 100644 --- a/Doughnut/DoughnutApp.swift +++ b/Doughnut/DoughnutApp.swift @@ -1,10 +1,20 @@ -// -// DoughnutApp.swift -// Doughnut -// -// Created by Chris Dyer on 19/10/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa diff --git a/Doughnut/Library/DownloadManager.swift b/Doughnut/Library/DownloadManager.swift index b2a86ab..4b84a8a 100644 --- a/Doughnut/Library/DownloadManager.swift +++ b/Doughnut/Library/DownloadManager.swift @@ -1,10 +1,20 @@ -// -// Download.swift -// Doughnut -// -// Created by Chris Dyer on 10/10/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Foundation import AVFoundation diff --git a/Doughnut/Library/Episode.swift b/Doughnut/Library/Episode.swift index 9812520..5e0d2d0 100644 --- a/Doughnut/Library/Episode.swift +++ b/Doughnut/Library/Episode.swift @@ -1,10 +1,20 @@ -// -// Episode.swift -// Doughnut -// -// Created by Chris Dyer on 29/09/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Foundation import AVFoundation diff --git a/Doughnut/Library/Library.swift b/Doughnut/Library/Library.swift index 49fd210..c6f4cca 100644 --- a/Doughnut/Library/Library.swift +++ b/Doughnut/Library/Library.swift @@ -1,10 +1,20 @@ -// -// Library.swift -// Doughnut -// -// Created by Chris Dyer on 27/09/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Foundation import FeedKit diff --git a/Doughnut/Library/MarkupGenerator.swift b/Doughnut/Library/MarkupGenerator.swift index fde55cd..ccbfb0e 100644 --- a/Doughnut/Library/MarkupGenerator.swift +++ b/Doughnut/Library/MarkupGenerator.swift @@ -1,11 +1,21 @@ -// -// MarkupGenerator.swift -// Doughnut -// -// Created by Chris Dyer on 16/10/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// - +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + import Foundation class MarkupGenerator { diff --git a/Doughnut/Library/Migrations.swift b/Doughnut/Library/Migrations.swift index 2b79e27..d76eecd 100644 --- a/Doughnut/Library/Migrations.swift +++ b/Doughnut/Library/Migrations.swift @@ -1,10 +1,20 @@ -// -// Migrations.swift -// Doughnut -// -// Created by Chris Dyer on 27/09/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Foundation import GRDB diff --git a/Doughnut/Library/Podcast.swift b/Doughnut/Library/Podcast.swift index 445a32c..1fb3058 100644 --- a/Doughnut/Library/Podcast.swift +++ b/Doughnut/Library/Podcast.swift @@ -1,10 +1,20 @@ -// -// Podcast.swift -// Doughnut -// -// Created by Chris Dyer on 28/09/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Foundation import GRDB diff --git a/Doughnut/Library/Utils.swift b/Doughnut/Library/Utils.swift index 60e184f..1be9949 100644 --- a/Doughnut/Library/Utils.swift +++ b/Doughnut/Library/Utils.swift @@ -1,10 +1,20 @@ -// -// Utils.swift -// Doughnut -// -// Created by Chris Dyer on 15/10/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Foundation diff --git a/Doughnut/Player.swift b/Doughnut/Player.swift index 208018d..0f04099 100644 --- a/Doughnut/Player.swift +++ b/Doughnut/Player.swift @@ -1,10 +1,20 @@ -// -// Player.swift -// Doughnut -// -// Created by Chris Dyer on 01/10/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa import AVFoundation diff --git a/Doughnut/Preference/PrefLibraryViewController.swift b/Doughnut/Preference/PrefLibraryViewController.swift index 193b346..1561b4d 100644 --- a/Doughnut/Preference/PrefLibraryViewController.swift +++ b/Doughnut/Preference/PrefLibraryViewController.swift @@ -1,10 +1,20 @@ -// -// PrefGeneralViewController.swift -// Doughnut -// -// Created by Chris Dyer on 16/10/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa import MASPreferences diff --git a/Doughnut/Preference/Preference.swift b/Doughnut/Preference/Preference.swift index 504db33..3495200 100644 --- a/Doughnut/Preference/Preference.swift +++ b/Doughnut/Preference/Preference.swift @@ -1,10 +1,20 @@ -// -// Preference.swift -// Doughnut -// -// Created by Chris Dyer on 27/09/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa diff --git a/Doughnut/View Controllers/DetailViewController.swift b/Doughnut/View Controllers/DetailViewController.swift index 2a7409e..189bb4d 100644 --- a/Doughnut/View Controllers/DetailViewController.swift +++ b/Doughnut/View Controllers/DetailViewController.swift @@ -1,10 +1,20 @@ -// -// DetailViewController.swift -// Doughnut -// -// Created by Chris Dyer on 23/09/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa import WebKit diff --git a/Doughnut/View Controllers/DownloadsViewController.swift b/Doughnut/View Controllers/DownloadsViewController.swift index b479ae6..32cd30e 100644 --- a/Doughnut/View Controllers/DownloadsViewController.swift +++ b/Doughnut/View Controllers/DownloadsViewController.swift @@ -1,10 +1,20 @@ -// -// DownloadsViewController.swift -// Doughnut -// -// Created by Chris Dyer on 11/10/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa diff --git a/Doughnut/View Controllers/EditPodcastViewController.swift b/Doughnut/View Controllers/EditPodcastViewController.swift index bc0edb5..f702beb 100644 --- a/Doughnut/View Controllers/EditPodcastViewController.swift +++ b/Doughnut/View Controllers/EditPodcastViewController.swift @@ -1,10 +1,20 @@ -// -// EditPodcastViewController.swift -// Doughnut -// -// Created by Chris Dyer on 30/11/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa diff --git a/Doughnut/View Controllers/EpisodeFilterViewController.swift b/Doughnut/View Controllers/EpisodeFilterViewController.swift index ee9db70..67b7490 100644 --- a/Doughnut/View Controllers/EpisodeFilterViewController.swift +++ b/Doughnut/View Controllers/EpisodeFilterViewController.swift @@ -1,10 +1,20 @@ -// -// EpisodeFilterViewController.swift -// Doughnut -// -// Created by Chris Dyer on 30/09/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa diff --git a/Doughnut/View Controllers/EpisodeViewController.swift b/Doughnut/View Controllers/EpisodeViewController.swift index 8148462..392d7d2 100644 --- a/Doughnut/View Controllers/EpisodeViewController.swift +++ b/Doughnut/View Controllers/EpisodeViewController.swift @@ -1,10 +1,20 @@ -// -// EpisodeViewController.swift -// Doughnut -// -// Created by Chris Dyer on 23/09/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa diff --git a/Doughnut/View Controllers/PodcastViewController.swift b/Doughnut/View Controllers/PodcastViewController.swift index 15b2c1a..9ed0f53 100644 --- a/Doughnut/View Controllers/PodcastViewController.swift +++ b/Doughnut/View Controllers/PodcastViewController.swift @@ -1,10 +1,20 @@ -// -// PodcastViewController.swift -// Doughnut -// -// Created by Chris Dyer on 23/09/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa diff --git a/Doughnut/View Controllers/SubscribeViewController.swift b/Doughnut/View Controllers/SubscribeViewController.swift index aaf6b6a..3b1796c 100644 --- a/Doughnut/View Controllers/SubscribeViewController.swift +++ b/Doughnut/View Controllers/SubscribeViewController.swift @@ -1,10 +1,20 @@ -// -// SubscribeViewController.swift -// Doughnut -// -// Created by Chris Dyer on 15/10/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa diff --git a/Doughnut/View Controllers/ViewController.swift b/Doughnut/View Controllers/ViewController.swift index bc1962a..0d306bd 100644 --- a/Doughnut/View Controllers/ViewController.swift +++ b/Doughnut/View Controllers/ViewController.swift @@ -1,10 +1,20 @@ -// -// ViewController.swift -// Doughnut -// -// Created by Chris Dyer on 22/09/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa diff --git a/Doughnut/Views/DetailWebView.swift b/Doughnut/Views/DetailWebView.swift index 4663a5f..99f6add 100644 --- a/Doughnut/Views/DetailWebView.swift +++ b/Doughnut/Views/DetailWebView.swift @@ -1,10 +1,20 @@ -// -// DetailWebView.swift -// Doughnut -// -// Created by Chris Dyer on 16/10/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa import WebKit diff --git a/Doughnut/Views/DownloadCellView.swift b/Doughnut/Views/DownloadCellView.swift index 8ad223d..d043e68 100644 --- a/Doughnut/Views/DownloadCellView.swift +++ b/Doughnut/Views/DownloadCellView.swift @@ -1,10 +1,20 @@ -// -// DownloadCellView.swift -// Doughnut -// -// Created by Chris Dyer on 11/10/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa diff --git a/Doughnut/Views/EpisodeCellView.swift b/Doughnut/Views/EpisodeCellView.swift index 87e588c..8b7f11c 100644 --- a/Doughnut/Views/EpisodeCellView.swift +++ b/Doughnut/Views/EpisodeCellView.swift @@ -1,10 +1,20 @@ -// -// EpisodeCellView.swift -// Doughnut -// -// Created by Chris Dyer on 27/09/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa diff --git a/Doughnut/Views/PlayerView.swift b/Doughnut/Views/PlayerView.swift index 1eb7383..c09d72f 100644 --- a/Doughnut/Views/PlayerView.swift +++ b/Doughnut/Views/PlayerView.swift @@ -1,10 +1,20 @@ -// -// PlayerView.swift -// Doughnut -// -// Created by Chris Dyer on 01/10/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa diff --git a/Doughnut/Views/PodcastCellView.swift b/Doughnut/Views/PodcastCellView.swift index a99d028..4592070 100644 --- a/Doughnut/Views/PodcastCellView.swift +++ b/Doughnut/Views/PodcastCellView.swift @@ -1,10 +1,20 @@ -// -// PodcastRowView.swift -// Doughnut -// -// Created by Chris Dyer on 27/09/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa diff --git a/Doughnut/Views/SeekSlider.swift b/Doughnut/Views/SeekSlider.swift index e3a0fef..e2d68fa 100644 --- a/Doughnut/Views/SeekSlider.swift +++ b/Doughnut/Views/SeekSlider.swift @@ -1,10 +1,20 @@ -// -// SeekSlider.swift -// Doughnut -// -// Created by Chris Dyer on 01/10/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa diff --git a/Doughnut/WindowController.swift b/Doughnut/WindowController.swift index 1081ad4..4a97b7c 100644 --- a/Doughnut/WindowController.swift +++ b/Doughnut/WindowController.swift @@ -1,10 +1,20 @@ -// -// WindowController.swift -// Doughnut -// -// Created by Chris Dyer on 23/09/2017. -// Copyright © 2017 Chris Dyer. All rights reserved. -// +/* + * Doughnut Podcast Client + * Copyright (C) 2017 Chris Dyer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Cocoa diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..18c6e1b --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/Pods/FeedKit/LICENSE b/Pods/FeedKit/LICENSE new file mode 100644 index 0000000..365f1c2 --- /dev/null +++ b/Pods/FeedKit/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Nuno Manuel Dias + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Pods/FeedKit/README.md b/Pods/FeedKit/README.md new file mode 100755 index 0000000..4fb4b52 --- /dev/null +++ b/Pods/FeedKit/README.md @@ -0,0 +1,233 @@ +![FeedKit](/FeedKit.png?raw=true) + +[![build status](https://travis-ci.org/nmdias/FeedKit.svg)](https://travis-ci.org/nmdias/FeedKit) +[![cocoapods compatible](https://img.shields.io/badge/cocoapods-compatible-brightgreen.svg)](https://cocoapods.org/pods/FeedKit) +[![carthage compatible](https://img.shields.io/badge/carthage-compatible-brightgreen.svg)](https://github.com/Carthage/Carthage) +[![language](https://img.shields.io/badge/spm-compatible-brightgreen.svg)](https://swift.org) +[![swift](https://img.shields.io/badge/swift-4-orange.svg)](https://github.com/nmdias/DefaultsKit/releases) + +## Features + +- [x] [Atom](https://tools.ietf.org/html/rfc4287) +- [x] RSS [0.90](http://www.rssboard.org/rss-0-9-0), [0.91](http://www.rssboard.org/rss-0-9-1), [1.00](http://web.resource.org/rss/1.0/spec), [2.00](http://cyber.law.harvard.edu/rss/rss.html) +- [x] [JSON](https://jsonfeed.org/version/1) +- [x] Namespaces + - [x] [Dublin Core](http://web.resource.org/rss/1.0/modules/dc/) + - [x] [Syndication](http://web.resource.org/rss/1.0/modules/syndication/) + - [x] [Content](http://web.resource.org/rss/1.0/modules/content/) + - [x] [Media RSS](http://www.rssboard.org/media-rss) + - [x] [iTunes Podcasting Tags](https://help.apple.com/itc/podcasts_connect/#/itcb54353390) +- [x] [Documentation](http://cocoadocs.org/docsets/FeedKit) +- [x] Unit Test Coverage + +## Requirements + +![xcode](https://img.shields.io/badge/xcode-9.0%2b-lightgrey.svg) +![ios](https://img.shields.io/badge/ios-8.0%2b-lightgrey.svg) +![tvos](https://img.shields.io/badge/tvos-9.0%2b-lightgrey.svg) +![watchos](https://img.shields.io/badge/watchos-2.0%2b-lightgrey.svg) +![mac os](https://img.shields.io/badge/mac%20os-10.10%2b-lightgrey.svg) +![mac os](https://img.shields.io/badge/ubuntu-16.04+-lightgrey.svg) + +Installation >> [`instructions`](https://github.com/nmdias/FeedKit/blob/master/INSTALL.md) << + +## Usage + +Build a URL pointing to an RSS, Atom or JSON Feed. +```swift +let feedURL = URL(string: "http://images.apple.com/main/rss/hotnews/hotnews.rss")! +``` + +Get an instance of `FeedParser` +```swift +let parser = FeedParser(URL: feedURL) // or FeedParser(data: data) +``` + +Then call `parse` or `parseAsync` to start parsing the feed... + +> A **common scenario** in UI environments would be parsing a feed **asynchronously** from a user initiated action, such as the touch of a button. e.g. + +```swift +// Parse asynchronously, not to block the UI. +parser.parseAsync(queue: DispatchQueue.global(qos: .userInitiated)) { (result) in + // Do your thing, then back to the Main thread + DispatchQueue.main.async { + // ..and update the UI + } +} +``` + +Remember, you are responsible to manually bring the result closure to whichever queue is apropriate. Usually to the Main thread, for UI apps, by calling `DispatchQueue.main.async` . + +Alternatively, you can also parse synchronously. + +```swift +let result = parser.parse() +``` + +## Parse Result + +Whichever the case, if parsing succeeds you should now have a `Strongly Typed Model` of an `RSS`, `Atom` or `JSON Feed`. +```swift +switch result { +case let .atom(feed): // Atom Syndication Format Feed Model +case let .rss(feed): // Really Simple Syndication Feed Model +case let .json(feed): // JSON Feed Model +case let .failure(error): +} +``` + + +#### Parse Success +You can check if a Feed was `successfully` parsed or not. +```swift +result.isSuccess // If parsing was a success +result.isFailure // If parsing failed +result.error // An error, if any +``` + +## Model Preview +Safely bind a feed of your choosing: +> You may find the example bellow useful, if you're dealing with only a single type of feed. +```swift +guard let feed = result.rssFeed, result.isSuccess else { + print(result.error) + return +} +``` +Then go through it's properties: + +> The RSS and Atom feed Models are rather extensive throughout the supported namespaces. These are just a preview of what's available. + +#### RSS + +```swift +feed.title +feed.link +feed.description +feed.language +feed.copyright +feed.managingEditor +feed.webMaster +feed.pubDate +feed.lastBuildDate +feed.categories +feed.generator +feed.docs +feed.cloud +feed.rating +feed.ttl +feed.image +feed.textInput +feed.skipHours +feed.skipDays +//... +feed.dublinCore +feed.syndication +feed.iTunes +// ... + +let item = feed.items?.first + +item?.title +item?.link +item?.description +item?.author +item?.categories +item?.comments +item?.enclosure +item?.guid +item?.pubDate +item?.source +//... +item?.dublinCore +item?.content +item?.iTunes +item?.media +// ... +``` + +> Refer to the [`documentation`](http://cocoadocs.org/docsets/FeedKit) for the complete model properties and descriptions + +#### Atom + +```swift +feed.title +feed.subtitle +feed.links +feed.updated +feed.authors +feed.contributors +feed.id +feed.generator +feed.icon +feed.logo +feed.rights +// ... + +let entry = feed.entries?.first + +entry?.title +entry?.summary +entry?.authors +entry?.contributors +entry?.links +entry?.updated +entry?.categories +entry?.id +entry?.content +entry?.published +entry?.source +entry?.rights +// ... +``` + +> Refer to the [`documentation`](http://cocoadocs.org/docsets/FeedKit) for the complete model properties and descriptions + +#### JSON + +```swift +feed.version +feed.title +feed.homePageURL +feed.feedUrl +feed.description +feed.userComment +feed.nextUrl +feed.icon +feed.favicon +feed.author +feed.expired +feed.hubs +feed.extensions +// ... + +let item = feed.items?.first + +item?.id +item?.url +item?.externalUrl +item?.title +item?.contentText +item?.contentHtml +item?.summary +item?.image +item?.bannerImage +item?.datePublished +item?.dateModified +item?.author +item?.url +item?.tags +item?.attachments +item?.extensions +// ... +``` + +> Refer to the [`documentation`](http://cocoadocs.org/docsets/FeedKit) for the complete model properties and descriptions + +## License + +FeedKit is released under the MIT license. See [LICENSE](https://github.com/nmdias/FeedKit/blob/master/LICENSE) for details. + + + diff --git a/Pods/FeedKit/Sources/FeedKit/Dates/DateSpec.swift b/Pods/FeedKit/Sources/FeedKit/Dates/DateSpec.swift new file mode 100644 index 0000000..1213585 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Dates/DateSpec.swift @@ -0,0 +1,39 @@ +// +// DateSpec.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Date specifications +/// +/// - rfc822: The `Standard for the format of arpa internet text messages`. +/// See https://www.ietf.org/rfc/rfc0822.txt +/// - rfc3999: The `Date and Time on the Internet: Timestamps`. +/// See https://www.ietf.org/rfc/rfc3339.txt +/// - iso8601: The `W3CDTF` date time format specification +/// See http://www.w3.org/TR/NOTE-datetime +enum DateSpec { + case rfc822 + case rfc3999 + case iso8601 +} diff --git a/Pods/FeedKit/Sources/FeedKit/Dates/ISO8601DateFormatter.swift b/Pods/FeedKit/Sources/FeedKit/Dates/ISO8601DateFormatter.swift new file mode 100644 index 0000000..546a9bd --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Dates/ISO8601DateFormatter.swift @@ -0,0 +1,58 @@ +// +// ISO8601DateFormatter.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Converts date and time textual representations within the ISO8601 +/// date specification into `Date` objects +class ISO8601DateFormatter: DateFormatter { + + let dateFormats = [ + "yyyy-mm-dd'T'hh:mm", + "yyyy-MM-dd'T'HH:mm:ssZZZZZ", + "yyyy-MM-dd'T'HH:mm:ss.SSZZZZZ", + "yyyy-MM-dd'T'HH:mmSSZZZZZ" + ] + + override init() { + super.init() + self.timeZone = TimeZone(secondsFromGMT: 0) + self.locale = Locale(identifier: "en_US_POSIX") + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) not supported") + } + + override func date(from string: String) -> Date? { + for dateFormat in self.dateFormats { + self.dateFormat = dateFormat + if let date = super.date(from: string) { + return date + } + } + return nil + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Dates/RFC3339DateFormatter.swift b/Pods/FeedKit/Sources/FeedKit/Dates/RFC3339DateFormatter.swift new file mode 100644 index 0000000..d1cac9c --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Dates/RFC3339DateFormatter.swift @@ -0,0 +1,57 @@ +// +// RFC3339DateFormatter.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Converts date and time textual representations within the RFC3339 +/// date specification into `Date` objects +class RFC3339DateFormatter: DateFormatter { + + let dateFormats = [ + "yyyy-MM-dd'T'HH:mm:ssZZZZZ", + "yyyy-MM-dd'T'HH:mm:ss.SSZZZZZ", + "yyyy-MM-dd'T'HH:mm:ss-SS:ZZ" + ] + + override init() { + super.init() + self.timeZone = TimeZone(secondsFromGMT: 0) + self.locale = Locale(identifier: "en_US_POSIX") + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) not supported") + } + + override func date(from string: String) -> Date? { + for dateFormat in self.dateFormats { + self.dateFormat = dateFormat + if let date = super.date(from: string) { + return date + } + } + return nil + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Dates/RFC822DateFormatter.swift b/Pods/FeedKit/Sources/FeedKit/Dates/RFC822DateFormatter.swift new file mode 100644 index 0000000..77e0f06 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Dates/RFC822DateFormatter.swift @@ -0,0 +1,57 @@ +// +// RFC822DateFormatter.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Converts date and time textual representations within the RFC822 +/// date specification into `Date` objects +class RFC822DateFormatter: DateFormatter { + + let dateFormats = [ + "EEE, d MMM yyyy HH:mm:ss zzz", + "EEE, d MMM yyyy HH:mm zzz", + "d MMM yyyy HH:mm:ss Z" + ] + + override init() { + super.init() + self.timeZone = TimeZone(secondsFromGMT: 0) + self.locale = Locale(identifier: "en_US_POSIX") + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) not supported") + } + + override func date(from string: String) -> Date? { + for dateFormat in self.dateFormats { + self.dateFormat = dateFormat + if let date = super.date(from: string) { + return date + } + } + return nil + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Extensions/Array + Equatable.swift b/Pods/FeedKit/Sources/FeedKit/Extensions/Array + Equatable.swift new file mode 100644 index 0000000..38e1396 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Extensions/Array + Equatable.swift @@ -0,0 +1,42 @@ +// +// Array + Equatable.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional arrays equatable +/// +/// - Parameters: +/// - lhs: The left-hand side +/// - rhs: The right-hand side +/// - Returns: A boolean value +public func ==(lhs: [T]?, rhs: [T]?) -> Bool { + switch (lhs,rhs) { + case (.some(let lhs), .some(let rhs)): + return lhs == rhs + case (.none, .none): + return true + default: + return false + } +} diff --git a/Pods/FeedKit/Sources/FeedKit/Extensions/Data + toUtf8.swift b/Pods/FeedKit/Sources/FeedKit/Extensions/Data + toUtf8.swift new file mode 100644 index 0000000..a0662c0 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Extensions/Data + toUtf8.swift @@ -0,0 +1,38 @@ +// +// String + toUtf8.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension Data { + + /// Detect encoding and convert data to UTF-8 + func toUtf8() -> Data? { + var convertedString: NSString? + let encoding = NSString.stringEncoding(for: self, encodingOptions: nil, convertedString: &convertedString, usedLossyConversion: nil) + + guard let str = NSString(data: self, encoding: encoding) as String? else { return nil } + return str.data(using: .utf8) + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Extensions/String + toBool.swift b/Pods/FeedKit/Sources/FeedKit/Extensions/String + toBool.swift new file mode 100644 index 0000000..13b0dce --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Extensions/String + toBool.swift @@ -0,0 +1,39 @@ +// +// String + toBool.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension String { + + /// Convert a string representation of a logical value to it's `Bool`. + /// equivalent + func toBool() -> Bool? { + switch self { + case "True", "true", "Yes", "yes", "1": return true + case "False", "false", "No", "no", "0": return false + default: return nil + } + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Extensions/String + toDate.swift b/Pods/FeedKit/Sources/FeedKit/Extensions/String + toDate.swift new file mode 100644 index 0000000..1270da6 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Extensions/String + toDate.swift @@ -0,0 +1,42 @@ +// +// String + toDate.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension String { + + /// Attempts to convert the textual representation of a date with + /// the specified `DateSpec` to an `Date` object. + /// + /// - Parameter spec: The `DateSpec` to interpert the string. + /// - Returns: A `Date` object, or nil if the conversion failed. + func toDate(from spec: DateSpec) -> Date? { + switch spec { + case .rfc822: return RFC822DateFormatter().date(from: self) + case .rfc3999: return RFC3339DateFormatter().date(from: self) + case .iso8601: return ISO8601DateFormatter().date(from: self) + } + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Extensions/String + toDuration.swift b/Pods/FeedKit/Sources/FeedKit/Extensions/String + toDuration.swift new file mode 100644 index 0000000..b22bbc9 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Extensions/String + toDuration.swift @@ -0,0 +1,54 @@ +// +// String + toDuration.swift +// +// Copyright (c) 2017 Ben Murphy +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension String { + + /// Convert the string representation of a time duration to a Time Interval. + /// + /// - Returns: A TimeInterval. + func toDuration() -> TimeInterval? { + let comps = self.components(separatedBy: ":") + + guard + !comps.contains(where: { Int($0) == nil }), + !comps.contains(where: { Int($0)! < 0 }) + else { return nil } + + return comps + .reversed() + .enumerated() + .map { i, e in + (Double(e) ?? 0) + * + pow(Double(60), Double(i)) + } + .reduce(0, +) + + } + +} + + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed + mapAttributes.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed + mapAttributes.swift new file mode 100644 index 0000000..3b5bec6 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed + mapAttributes.swift @@ -0,0 +1,354 @@ +// +// AtomFeed + mapAttributes.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension AtomFeed { + + /// Maps the attributes of the specified dictionary for a given `AtomPath` + /// to the `AtomFeed` model + /// + /// - Parameters: + /// - attributes: The attribute dictionary to map to the model. + /// - path: The path of feed's element. + func map(_ attributes: [String : String], for path: AtomPath) { + + switch path { + + case .feedSubtitle: + + if self.subtitle == nil { + self.subtitle = AtomFeedSubtitle(attributes: attributes) + } + + case .feedLink: + + if self.links == nil { + self.links = [] + } + + self.links?.append(AtomFeedLink(attributes: attributes)) + + case .feedCategory: + + if self.categories == nil { + self.categories = [] + } + + self.categories?.append(AtomFeedCategory(attributes: attributes)) + + case .feedAuthor: + + if self.authors == nil { + self.authors = [] + } + + self.authors?.append(AtomFeedAuthor()) + + case .feedContributor: + + if self.contributors == nil { + self.contributors = [] + } + + self.contributors?.append(AtomFeedContributor()) + + case .feedGenerator: + + if self.generator == nil { + self.generator = AtomFeedGenerator(attributes: attributes) + } + + case .feedEntry: + + if self.entries == nil { + self.entries = [] + } + + self.entries?.append(AtomFeedEntry()) + + case .feedEntrySummary: + + if self.entries?.last?.summary == nil { + self.entries?.last?.summary = AtomFeedEntrySummary(attributes: attributes) + } + + case .feedEntryAuthor: + + if self.entries?.last?.authors == nil { + self.entries?.last?.authors = [] + } + + self.entries?.last?.authors?.append(AtomFeedEntryAuthor()) + + case .feedEntryContributor: + + if self.entries?.last?.contributors == nil { + self.entries?.last?.contributors = [] + } + + self.entries?.last?.contributors?.append(AtomFeedEntryContributor()) + + case .feedEntryLink: + + if self.entries?.last?.links == nil { + self.entries?.last?.links = [] + } + + self.entries?.last?.links?.append(AtomFeedEntryLink(attributes: attributes)) + + case .feedEntryCategory: + + if self.entries?.last?.categories == nil { + self.entries?.last?.categories = [] + } + + self.entries?.last?.categories?.append(AtomFeedEntryCategory(attributes: attributes)) + + case .feedEntryContent: + + if self.entries?.last?.content == nil { + self.entries?.last?.content = AtomFeedEntryContent(attributes: attributes) + } + + case .feedEntrySource: + + if self.entries?.last?.source == nil { + self.entries?.last?.source = AtomFeedEntrySource() + } + + // MARK: Media + + case + .feedEntryMediaThumbnail, + .feedEntryMediaContent, + .feedEntryMediaCommunity, + .feedEntryMediaCommunityMediaStarRating, + .feedEntryMediaCommunityMediaStatistics, + .feedEntryMediaCommunityMediaTags, + .feedEntryMediaComments, + .feedEntryMediaCommentsMediaComment, + .feedEntryMediaEmbed, + .feedEntryMediaEmbedMediaParam, + .feedEntryMediaResponses, + .feedEntryMediaResponsesMediaResponse, + .feedEntryMediaBackLinks, + .feedEntryMediaBackLinksBackLink, + .feedEntryMediaStatus, + .feedEntryMediaPrice, + .feedEntryMediaLicense, + .feedEntryMediaSubTitle, + .feedEntryMediaPeerLink, + .feedEntryMediaLocation, + .feedEntryMediaLocationPosition, + .feedEntryMediaRestriction, + .feedEntryMediaScenes, + .feedEntryMediaScenesMediaScene, + .feedEntryMediaGroup, + .feedEntryMediaGroupMediaCategory, + .feedEntryMediaGroupMediaCredit, + .feedEntryMediaGroupMediaRating, + .feedEntryMediaGroupMediaContent: + + if self.entries?.last?.media == nil { + self.entries?.last?.media = MediaNamespace() + } + + switch path { + + case .feedEntryMediaThumbnail: + + if self.entries?.last?.media?.mediaThumbnails == nil { + self.entries?.last?.media?.mediaThumbnails = [] + } + + self.entries?.last?.media?.mediaThumbnails?.append(MediaThumbnail(attributes: attributes)) + + case .feedEntryMediaContent: + + if self.entries?.last?.media?.mediaContents == nil { + self.entries?.last?.media?.mediaContents = [] + } + + self.entries?.last?.media?.mediaContents?.append(MediaContent(attributes: attributes)) + + case .feedEntryMediaCommunity: + + if self.entries?.last?.media?.mediaCommunity == nil { + self.entries?.last?.media?.mediaCommunity = MediaCommunity() + } + + case .feedEntryMediaCommunityMediaStarRating: + + if self.entries?.last?.media?.mediaCommunity?.mediaStarRating == nil { + self.entries?.last?.media?.mediaCommunity?.mediaStarRating = MediaStarRating(attributes: attributes) + } + + case .feedEntryMediaCommunityMediaStatistics: + + if self.entries?.last?.media?.mediaCommunity?.mediaStatistics == nil { + self.entries?.last?.media?.mediaCommunity?.mediaStatistics = MediaStatistics(attributes: attributes) + } + + case .feedEntryMediaCommunityMediaTags: + + if self.entries?.last?.media?.mediaCommunity?.mediaTags == nil { + self.entries?.last?.media?.mediaCommunity?.mediaTags = [] + } + + case .feedEntryMediaComments: + + if self.entries?.last?.media?.mediaComments == nil { + self.entries?.last?.media?.mediaComments = [] + } + + case .feedEntryMediaEmbed: + + if self.entries?.last?.media?.mediaEmbed == nil { + self.entries?.last?.media?.mediaEmbed = MediaEmbed(attributes: attributes) + } + + case .feedEntryMediaEmbedMediaParam: + + if self.entries?.last?.media?.mediaEmbed?.mediaParams == nil { + self.entries?.last?.media?.mediaEmbed?.mediaParams = [] + } + + self.entries?.last?.media?.mediaEmbed?.mediaParams?.append(MediaParam(attributes: attributes)) + + case .feedEntryMediaResponses: + + if self.entries?.last?.media?.mediaResponses == nil { + self.entries?.last?.media?.mediaResponses = [] + } + + case .feedEntryMediaBackLinks: + + if self.entries?.last?.media?.mediaBackLinks == nil { + self.entries?.last?.media?.mediaBackLinks = [] + } + + case .feedEntryMediaStatus: + + if self.entries?.last?.media?.mediaStatus == nil { + self.entries?.last?.media?.mediaStatus = MediaStatus(attributes: attributes) + } + + case .feedEntryMediaPrice: + + if self.entries?.last?.media?.mediaPrices == nil { + self.entries?.last?.media?.mediaPrices = [] + } + + self.entries?.last?.media?.mediaPrices?.append(MediaPrice(attributes: attributes)) + + case .feedEntryMediaLicense: + + if self.entries?.last?.media?.mediaLicense == nil { + self.entries?.last?.media?.mediaLicense = MediaLicence(attributes: attributes) + } + + case .feedEntryMediaSubTitle: + + if self.entries?.last?.media?.mediaSubTitle == nil { + self.entries?.last?.media?.mediaSubTitle = MediaSubTitle(attributes: attributes) + } + + case .feedEntryMediaPeerLink: + + if self.entries?.last?.media?.mediaPeerLink == nil { + self.entries?.last?.media?.mediaPeerLink = MediaPeerLink(attributes: attributes) + } + + case .feedEntryMediaLocation: + + if self.entries?.last?.media?.mediaLocation == nil { + self.entries?.last?.media?.mediaLocation = MediaLocation(attributes: attributes) + } + + case .feedEntryMediaRestriction: + + if self.entries?.last?.media?.mediaRestriction == nil { + self.entries?.last?.media?.mediaRestriction = MediaRestriction(attributes: attributes) + } + + case .feedEntryMediaScenes: + + if self.entries?.last?.media?.mediaScenes == nil { + self.entries?.last?.media?.mediaScenes = [] + } + + case .feedEntryMediaScenesMediaScene: + + if self.entries?.last?.media?.mediaScenes == nil { + self.entries?.last?.media?.mediaScenes = [] + } + + self.entries?.last?.media?.mediaScenes?.append(MediaScene()) + + case .feedEntryMediaGroup: + + if self.entries?.last?.media?.mediaGroup == nil { + self.entries?.last?.media?.mediaGroup = MediaGroup() + } + + case .feedEntryMediaGroupMediaCategory: + + if self.entries?.last?.media?.mediaGroup?.mediaCategory == nil { + self.entries?.last?.media?.mediaGroup?.mediaCategory = MediaCategory(attributes: attributes) + } + + case .feedEntryMediaGroupMediaCredit: + + if self.entries?.last?.media?.mediaGroup?.mediaCredits == nil { + self.entries?.last?.media?.mediaGroup?.mediaCredits = [] + } + + self.entries?.last?.media?.mediaGroup?.mediaCredits?.append(MediaCredit(attributes: attributes)) + + case .feedEntryMediaGroupMediaRating: + + if self.entries?.last?.media?.mediaGroup?.mediaRating == nil { + self.entries?.last?.media?.mediaGroup?.mediaRating = MediaRating(attributes: attributes) + } + + case .feedEntryMediaGroupMediaContent: + + if self.entries?.last?.media?.mediaGroup?.mediaContents == nil { + self.entries?.last?.media?.mediaGroup?.mediaContents = [] + } + + self.entries?.last?.media?.mediaGroup?.mediaContents?.append(MediaContent(attributes: attributes)) + + default: break + + } + + default: break + + } + + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed + mapCharacters.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed + mapCharacters.swift new file mode 100644 index 0000000..ec0e024 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed + mapCharacters.swift @@ -0,0 +1,86 @@ +// +// AtomFeed + mapCharacters.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension AtomFeed { + + /// Maps the characters in the specified string to the `AtomFeed` model. + /// + /// - Parameters: + /// - string: The string to map to the model. + /// - path: The path of feed's element. + func map(_ string: String, for path: AtomPath) { + switch path { + case .feedTitle: self.title = self.title?.appending(string) ?? string + case .feedSubtitle: self.subtitle?.value = self.subtitle?.value?.appending(string) ?? string + case .feedUpdated: self.updated = string.toDate(from: .rfc3999) + case .feedAuthorName: self.authors?.last?.name = self.authors?.last?.name?.appending(string) ?? string + case .feedAuthorEmail: self.authors?.last?.email = self.authors?.last?.email?.appending(string) ?? string + case .feedAuthorUri: self.authors?.last?.uri = self.authors?.last?.uri?.appending(string) ?? string + case .feedContributorName: self.contributors?.last?.name = self.contributors?.last?.name?.appending(string) ?? string + case .feedContributorEmail: self.contributors?.last?.email = self.contributors?.last?.email?.appending(string) ?? string + case .feedContributorUri: self.contributors?.last?.uri = self.contributors?.last?.uri?.appending(string) ?? string + case .feedID: self.id = self.id?.appending(string) ?? string + case .feedGenerator: self.generator?.value = self.generator?.value?.appending(string) ?? string + case .feedIcon: self.icon = self.icon?.appending(string) ?? string + case .feedLogo: self.logo = self.logo?.appending(string) ?? string + case .feedRights: self.rights = self.rights?.appending(string) ?? string + case .feedEntryTitle: self.entries?.last?.title = self.entries?.last?.title?.appending(string) ?? string + case .feedEntrySummary: self.entries?.last?.summary?.value = self.entries?.last?.summary?.value?.appending(string) ?? string + case .feedEntryUpdated: self.entries?.last?.updated = string.toDate(from: .rfc3999) + case .feedEntryID: self.entries?.last?.id = self.entries?.last?.id?.appending(string) ?? string + case .feedEntryContent: self.entries?.last?.content?.value = self.entries?.last?.content?.value?.appending(string) ?? string + case .feedEntryPublished: self.entries?.last?.published = string.toDate(from: .rfc3999) + case .feedEntrySourceID: self.entries?.last?.source?.id = self.entries?.last?.source?.id?.appending(string) ?? string + case .feedEntrySourceTitle: self.entries?.last?.source?.title = self.entries?.last?.source?.title?.appending(string) ?? string + case .feedEntrySourceUpdated: self.entries?.last?.source?.updated = string.toDate(from: .rfc3999) + case .feedEntryRights: self.entries?.last?.rights = self.entries?.last?.rights?.appending(string) ?? string + case .feedEntryAuthorName: self.entries?.last?.authors?.last?.name = self.entries?.last?.authors?.last?.name?.appending(string) ?? string + case .feedEntryAuthorEmail: self.entries?.last?.authors?.last?.email = self.entries?.last?.authors?.last?.email?.appending(string) ?? string + case .feedEntryAuthorUri: self.entries?.last?.authors?.last?.uri = self.entries?.last?.authors?.last?.uri?.appending(string) ?? string + case .feedEntryContributorName: self.entries?.last?.contributors?.last?.name = self.entries?.last?.contributors?.last?.name?.appending(string) ?? string + case .feedEntryContributorEmail: self.entries?.last?.contributors?.last?.email = self.entries?.last?.contributors?.last?.email?.appending(string) ?? string + case .feedEntryContributorUri: self.entries?.last?.contributors?.last?.uri = self.entries?.last?.contributors?.last?.uri?.appending(string) ?? string + case .feedEntryMediaThumbnail: self.entries?.last?.media?.mediaThumbnails?.last?.value = self.entries?.last?.media?.mediaThumbnails?.last?.value?.appending(string) ?? string + case .feedEntryMediaLicense: self.entries?.last?.media?.mediaLicense?.value = self.entries?.last?.media?.mediaLicense?.value?.appending(string) ?? string + case .feedEntryMediaRestriction: self.entries?.last?.media?.mediaRestriction?.value = self.entries?.last?.media?.mediaRestriction?.value?.appending(string) ?? string + case .feedEntryMediaCommunityMediaTags: self.entries?.last?.media?.mediaCommunity?.mediaTags = MediaTag.tagsFrom(string: string) + case .feedEntryMediaCommentsMediaComment: self.entries?.last?.media?.mediaComments?.append(string) + case .feedEntryMediaEmbedMediaParam: self.entries?.last?.media?.mediaEmbed?.mediaParams?.last?.value = self.entries?.last?.media?.mediaEmbed?.mediaParams?.last?.value?.appending(string) ?? string + case .feedEntryMediaGroupMediaCredit: self.entries?.last?.media?.mediaGroup?.mediaCredits?.last?.value = self.entries?.last?.media?.mediaGroup?.mediaCredits?.last?.value?.appending(string) ?? string + case .feedEntryMediaGroupMediaCategory: self.entries?.last?.media?.mediaGroup?.mediaCategory?.value = self.entries?.last?.media?.mediaGroup?.mediaCategory?.value?.appending(string) ?? string + case .feedEntryMediaGroupMediaRating: self.entries?.last?.media?.mediaGroup?.mediaRating?.value = self.entries?.last?.media?.mediaGroup?.mediaRating?.value?.appending(string) ?? string + case .feedEntryMediaResponsesMediaResponse: self.entries?.last?.media?.mediaResponses?.append(string) + case .feedEntryMediaBackLinksBackLink: self.entries?.last?.media?.mediaBackLinks?.append(string) + case .feedEntryMediaLocationPosition: self.entries?.last?.media?.mediaLocation?.mapFrom(latLng: string) + case .feedEntryMediaScenesMediaSceneSceneTitle: self.entries?.last?.media?.mediaScenes?.last?.sceneTitle = self.entries?.last?.media?.mediaScenes?.last?.sceneTitle?.appending(string) ?? string + case .feedEntryMediaScenesMediaSceneSceneDescription: self.entries?.last?.media?.mediaScenes?.last?.sceneDescription = self.entries?.last?.media?.mediaScenes?.last?.sceneDescription?.appending(string) ?? string + case .feedEntryMediaScenesMediaSceneSceneStartTime: self.entries?.last?.media?.mediaScenes?.last?.sceneStartTime = string.toDuration() + case .feedEntryMediaScenesMediaSceneSceneEndTime: self.entries?.last?.media?.mediaScenes?.last?.sceneEndTime = string.toDuration() + default: break + } + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed.swift new file mode 100644 index 0000000..5eb1602 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed.swift @@ -0,0 +1,188 @@ +// +// AtomFeed.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Data model for the XML DOM of the Atom Specification +/// See https://tools.ietf.org/html/rfc4287 +/// +/// The "atom:feed" element is the document (i.e., top-level) element of +/// an Atom Feed Document, acting as a container for metadata and data +/// associated with the feed. Its element children consist of metadata +/// elements followed by zero or more atom:entry child elements. +open class AtomFeed { + + /// The "atom:title" element is a Text construct that conveys a human- + /// readable title for an entry or feed. + public var title: String? + + /// The "atom:subtitle" element is a Text construct that conveys a human- + /// readable description or subtitle for a feed. + public var subtitle: AtomFeedSubtitle? + + /// The "atom:link" element defines a reference from an entry or feed to + /// a Web resource. This specification assigns no meaning to the content + /// (if any) of this element. + public var links: [AtomFeedLink]? + + /// The "atom:updated" element is a Date construct indicating the most + /// recent instant in time when an entry or feed was modified in a way + /// the publisher considers significant. Therefore, not all + /// modifications necessarily result in a changed atom:updated value. + public var updated: Date? + + /// The "atom:category" element conveys information about a category + /// associated with an entry or feed. This specification assigns no + /// meaning to the content (if any) of this element. + public var categories: [AtomFeedCategory]? + + /// The "atom:author" element is a Person construct that indicates the + /// author of the entry or feed. + /// + /// If an atom:entry element does not contain atom:author elements, then + /// the atom:author elements of the contained atom:source element are + /// considered to apply. In an Atom Feed Document, the atom:author + /// elements of the containing atom:feed element are considered to apply + /// to the entry if there are no atom:author elements in the locations + /// described above. + public var authors: [AtomFeedAuthor]? + + /// The "atom:contributor" element is a Person construct that indicates a + /// person or other entity who contributed to the entry or feed. + public var contributors: [AtomFeedContributor]? + + /// The "atom:id" element conveys a permanent, universally unique + /// identifier for an entry or feed. + /// + /// Its content MUST be an IRI, as defined by [RFC3987]. Note that the + /// definition of "IRI" excludes relative references. Though the IRI + /// might use a dereferencable scheme, Atom Processors MUST NOT assume it + /// can be dereferenced. + /// + /// When an Atom Document is relocated, migrated, syndicated, + /// republished, exported, or imported, the content of its atom:id + /// element MUST NOT change. Put another way, an atom:id element + /// pertains to all instantiations of a particular Atom entry or feed; + /// revisions retain the same content in their atom:id elements. It is + /// suggested that the atom:id element be stored along with the + /// associated resource. + /// + /// The content of an atom:id element MUST be created in a way that + /// assures uniqueness. + /// + /// Because of the risk of confusion between IRIs that would be + /// equivalent if they were mapped to URIs and dereferenced, the + /// following normalization strategy SHOULD be applied when generating + /// atom:id elements: + /// + /// - Provide the scheme in lowercase characters. + /// - Provide the host, if any, in lowercase characters. + /// - Only perform percent-encoding where it is essential. + /// - Use uppercase A through F characters when percent-encoding. + /// - Prevent dot-segments from appearing in paths. + /// - For schemes that define a default authority, use an empty + /// authority if the default is desired. + /// - For schemes that define an empty path to be equivalent to a path + /// of "/", use "/". + /// - For schemes that define a port, use an empty port if the default + /// is desired. + /// - Preserve empty fragment identifiers and queries. + /// - Ensure that all components of the IRI are appropriately character + /// normalized, e.g., by using NFC or NFKC. + public var id: String? + + /// The "atom:generator" element's content identifies the agent used to + /// generate a feed, for debugging and other purposes. + /// + /// The content of this element, when present, MUST be a string that is a + /// human-readable name for the generating agent. Entities such as + /// "&" and "<" represent their corresponding characters ("&" and + /// "<" respectively), not markup. + /// + /// The atom:generator element MAY have a "uri" attribute whose value + /// MUST be an IRI reference [RFC3987]. When dereferenced, the resulting + /// URI (mapped from an IRI, if necessary) SHOULD produce a + /// representation that is relevant to that agent. + /// + /// The atom:generator element MAY have a "version" attribute that + /// indicates the version of the generating agent. + public var generator: AtomFeedGenerator? + + /// The "atom:icon" element's content is an IRI reference [RFC3987] that + /// identifies an image that provides iconic visual identification for a + /// feed. + /// + /// The image SHOULD have an aspect ratio of one (horizontal) to one + /// (vertical) and SHOULD be suitable for presentation at a small size. + public var icon: String? + + /// The "atom:logo" element's content is an IRI reference [RFC3987] that + /// identifies an image that provides visual identification for a feed. + /// + /// The image SHOULD have an aspect ratio of 2 (horizontal) to 1 + /// (vertical). + public var logo: String? + + /// The "atom:rights" element is a Text construct that conveys + /// information about rights held in and over an entry or feed. + /// + /// The atom:rights element SHOULD NOT be used to convey machine-readable + /// licensing information. + /// + /// If an atom:entry element does not contain an atom:rights element, + /// then the atom:rights element of the containing atom:feed element, if + /// present, is considered to apply to the entry. + public var rights: String? + + /// The "atom:entry" element represents an individual entry, acting as a + /// container for metadata and data associated with the entry. This + /// element can appear as a child of the atom:feed element, or it can + /// appear as the document (i.e., top-level) element of a stand-alone + /// Atom Entry Document. + public var entries: [AtomFeedEntry]? + +} + +// MARK: - Equatable + +extension AtomFeed: Equatable { + + public static func ==(lhs: AtomFeed, rhs: AtomFeed) -> Bool { + return + lhs.title == rhs.title && + lhs.subtitle == rhs.subtitle && + lhs.links == rhs.links && + lhs.updated == rhs.updated && + lhs.categories == rhs.categories && + lhs.authors == rhs.authors && + lhs.contributors == rhs.contributors && + lhs.id == rhs.id && + lhs.generator == rhs.generator && + lhs.icon == rhs.icon && + lhs.logo == rhs.logo && + lhs.rights == rhs.rights && + lhs.entries == rhs.entries + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedAuthor.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedAuthor.swift new file mode 100644 index 0000000..f701193 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedAuthor.swift @@ -0,0 +1,68 @@ +// +// AtomFeedAuthor.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:author" element is a Person construct that indicates the +/// author of the entry or feed. +/// +/// If an atom:entry element does not contain atom:author elements, then +/// the atom:author elements of the contained atom:source element are +/// considered to apply. In an Atom Feed Document, the atom:author +/// elements of the containing atom:feed element are considered to apply +/// to the entry if there are no atom:author elements in the locations +/// described above. +public class AtomFeedAuthor { + + /// The "atom:name" element's content conveys a human-readable name for + /// the person. The content of atom:name is Language-Sensitive. Person + /// constructs MUST contain exactly one "atom:name" element. + public var name: String? + + /// The "atom:email" element's content conveys an e-mail address + /// associated with the person. Person constructs MAY contain an + /// atom:email element, but MUST NOT contain more than one. Its content + /// MUST conform to the "addr-spec" production in [RFC2822]. + public var email: String? + + /// The "atom:uri" element's content conveys an IRI associated with the + /// person. Person constructs MAY contain an atom:uri element, but MUST + /// NOT contain more than one. The content of atom:uri in a Person + /// construct MUST be an IRI reference [RFC3987]. + public var uri: String? + +} + +// MARK: - Equatable + +extension AtomFeedAuthor: Equatable { + + public static func ==(lhs: AtomFeedAuthor, rhs: AtomFeedAuthor) -> Bool { + return + lhs.name == rhs.name && + lhs.email == rhs.email && + lhs.uri == rhs.uri + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedCategory.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedCategory.swift new file mode 100644 index 0000000..b06a0cd --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedCategory.swift @@ -0,0 +1,107 @@ +// +// AtomFeedCategory.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:category" element conveys information about a category +/// associated with an entry or feed. This specification assigns no +/// meaning to the content (if any) of this element. +public class AtomFeedCategory { + + /// The element's attributes. + public class Attributes { + + /// The "term" attribute is a string that identifies the category to + /// which the entry or feed belongs. Category elements MUST have a + /// "term" attribute. + public var term: String? + + /// The "scheme" attribute is an IRI that identifies a categorization + /// scheme. Category elements MAY have a "scheme" attribute. + public var scheme: String? + + /// The "label" attribute provides a human-readable label for display in + /// end-user applications. The content of the "label" attribute is + /// Language-Sensitive. Entities such as "&" and "<" represent + /// their corresponding characters ("&" and "<", respectively), not + /// markup. Category elements MAY have a "label" attribute. + public var label: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + +} + +// MARK: - Initializers + +extension AtomFeedCategory { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = AtomFeedCategory.Attributes(attributes: attributeDict) + } + +} + +extension AtomFeedCategory.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.term = attributeDict["term"] + self.scheme = attributeDict["scheme"] + self.label = attributeDict["label"] + + } + +} + +// MARK: - Equatable + +extension AtomFeedCategory: Equatable { + + public static func ==(lhs: AtomFeedCategory, rhs: AtomFeedCategory) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension AtomFeedCategory.Attributes: Equatable { + + public static func ==(lhs: AtomFeedCategory.Attributes, rhs: AtomFeedCategory.Attributes) -> Bool { + return + lhs.term == rhs.term && + lhs.scheme == rhs.scheme && + lhs.label == rhs.label + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedContributor.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedContributor.swift new file mode 100644 index 0000000..a62e428 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedContributor.swift @@ -0,0 +1,61 @@ +// +// AtomFeedContributor.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:contributor" element is a Person construct that indicates a +/// person or other entity who contributed to the entry or feed. +public class AtomFeedContributor { + + /// The "atom:name" element's content conveys a human-readable name for + /// the person. The content of atom:name is Language-Sensitive. Person + /// constructs MUST contain exactly one "atom:name" element. + public var name: String? + + /// The "atom:email" element's content conveys an e-mail address + /// associated with the person. Person constructs MAY contain an + /// atom:email element, but MUST NOT contain more than one. Its content + /// MUST conform to the "addr-spec" production in [RFC2822]. + public var email: String? + + /// The "atom:uri" element's content conveys an IRI associated with the + /// person. Person constructs MAY contain an atom:uri element, but MUST + /// NOT contain more than one. The content of atom:uri in a Person + /// construct MUST be an IRI reference [RFC3987]. + public var uri: String? + +} + +// MARK: - Equatable + +extension AtomFeedContributor: Equatable { + + public static func ==(lhs: AtomFeedContributor, rhs: AtomFeedContributor) -> Bool { + return + lhs.name == rhs.name && + lhs.email == rhs.email && + lhs.uri == rhs.uri + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntry.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntry.swift new file mode 100644 index 0000000..991dbc2 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntry.swift @@ -0,0 +1,189 @@ +// +// AtomFeedEntry.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:entry" element represents an individual entry, acting as a +/// container for metadata and data associated with the entry. This +/// element can appear as a child of the atom:feed element, or it can +/// appear as the document (i.e., top-level) element of a stand-alone +/// Atom Entry Document. +public class AtomFeedEntry { + + /// The "atom:title" element is a Text construct that conveys a human- + /// readable title for an entry or feed. + public var title: String? + + /// The "atom:summary" element is a Text construct that conveys a short + /// summary, abstract, or excerpt of an entry. + /// + /// atomSummary = element atom:summary { atomTextConstruct } + /// + /// It is not advisable for the atom:summary element to duplicate + /// atom:title or atom:content because Atom Processors might assume there + /// is a useful summary when there is none. + public var summary: AtomFeedEntrySummary? + + /// The "atom:author" element is a Person construct that indicates the + /// author of the entry or feed. + /// + /// If an atom:entry element does not contain atom:author elements, then + /// the atom:author elements of the contained atom:source element are + /// considered to apply. In an Atom Feed Document, the atom:author + /// elements of the containing atom:feed element are considered to apply + /// to the entry if there are no atom:author elements in the locations + /// described above. + public var authors: [AtomFeedEntryAuthor]? + + /// The "atom:contributor" element is a Person construct that indicates a + /// person or other entity who contributed to the entry or feed. + public var contributors: [AtomFeedEntryContributor]? + + /// The "atom:link" element defines a reference from an entry or feed to + /// a Web resource. This specification assigns no meaning to the content + /// (if any) of this element. + public var links: [AtomFeedEntryLink]? + + /// The "atom:updated" element is a Date construct indicating the most + /// recent instant in time when an entry or feed was modified in a way + /// the publisher considers significant. Therefore, not all + /// modifications necessarily result in a changed atom:updated value. + /// + /// Publishers MAY change the value of this element over time. + public var updated: Date? + + /// The "atom:category" element conveys information about a category + /// associated with an entry or feed. This specification assigns no + /// meaning to the content (if any) of this element. + public var categories: [AtomFeedEntryCategory]? + + /// The "atom:id" element conveys a permanent, universally unique + /// identifier for an entry or feed. + /// + /// Its content MUST be an IRI, as defined by [RFC3987]. Note that the + /// definition of "IRI" excludes relative references. Though the IRI + /// might use a dereferencable scheme, Atom Processors MUST NOT assume it + /// can be dereferenced. + /// + /// When an Atom Document is relocated, migrated, syndicated, + /// republished, exported, or imported, the content of its atom:id + /// element MUST NOT change. Put another way, an atom:id element + /// pertains to all instantiations of a particular Atom entry or feed; + /// revisions retain the same content in their atom:id elements. It is + /// suggested that the atom:id element be stored along with the + /// associated resource. + /// + /// The content of an atom:id element MUST be created in a way that + /// assures uniqueness. + /// + /// Because of the risk of confusion between IRIs that would be + /// equivalent if they were mapped to URIs and dereferenced, the + /// following normalization strategy SHOULD be applied when generating + /// atom:id elements: + /// + /// - Provide the scheme in lowercase characters. + /// - Provide the host, if any, in lowercase characters. + /// - Only perform percent-encoding where it is essential. + /// - Use uppercase A through F characters when percent-encoding. + /// - Prevent dot-segments from appearing in paths. + /// - For schemes that define a default authority, use an empty + /// authority if the default is desired. + /// - For schemes that define an empty path to be equivalent to a path + /// of "/", use "/". + /// - For schemes that define a port, use an empty port if the default + /// is desired. + /// - Preserve empty fragment identifiers and queries. + /// - Ensure that all components of the IRI are appropriately character + /// normalized, e.g., by using NFC or NFKC. + public var id: String? + + /// The "atom:content" element either contains or links to the content of + /// the entry. The content of atom:content is Language-Sensitive. + public var content: AtomFeedEntryContent? + + /// The "atom:published" element is a Date construct indicating an + /// instant in time associated with an event early in the life cycle of + /// the entry. + /// + /// Typically, atom:published will be associated with the initial + /// creation or first availability of the resource. + public var published: Date? + + /// If an atom:entry is copied from one feed into another feed, then the + /// source atom:feed's metadata (all child elements of atom:feed other + /// than the atom:entry elements) MAY be preserved within the copied + /// entry by adding an atom:source child element, if it is not already + /// present in the entry, and including some or all of the source feed's + /// Metadata elements as the atom:source element's children. Such + /// metadata SHOULD be preserved if the source atom:feed contains any of + /// the child elements atom:author, atom:contributor, atom:rights, or + /// atom:category and those child elements are not present in the source + /// atom:entry. + /// + /// The atom:source element is designed to allow the aggregation of + /// entries from different feeds while retaining information about an + /// entry's source feed. For this reason, Atom Processors that are + /// performing such aggregation SHOULD include at least the required + /// feed-level Metadata elements (atom:id, atom:title, and atom:updated) + /// in the atom:source element. + public var source: AtomFeedEntrySource? + + /// The "atom:rights" element is a Text construct that conveys + /// information about rights held in and over an entry or feed. + /// + /// The atom:rights element SHOULD NOT be used to convey machine-readable + /// licensing information. + /// + /// If an atom:entry element does not contain an atom:rights element, + /// then the atom:rights element of the containing atom:feed element, if + /// present, is considered to apply to the entry. + public var rights: String? + + /// Media RSS is a new RSS module that supplements the + /// capabilities of RSS 2.0. + public var media: MediaNamespace? + +} + +// MARK: - Equatable + +extension AtomFeedEntry: Equatable { + + public static func ==(lhs: AtomFeedEntry, rhs: AtomFeedEntry) -> Bool { + return + lhs.title == rhs.title && + lhs.summary == rhs.summary && + lhs.authors == rhs.authors && + lhs.contributors == rhs.contributors && + lhs.links == rhs.links && + lhs.updated == rhs.updated && + lhs.categories == rhs.categories && + lhs.id == rhs.id && + lhs.content == rhs.content && + lhs.published == rhs.published && + lhs.source == rhs.source && + lhs.rights == rhs.rights + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryAuthor.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryAuthor.swift new file mode 100644 index 0000000..0cc9d85 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryAuthor.swift @@ -0,0 +1,68 @@ +// +// AtomFeedEntryAuthor.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:author" element is a Person construct that indicates the +/// author of the entry or feed. +/// +/// If an atom:entry element does not contain atom:author elements, then +/// the atom:author elements of the contained atom:source element are +/// considered to apply. In an Atom Feed Document, the atom:author +/// elements of the containing atom:feed element are considered to apply +/// to the entry if there are no atom:author elements in the locations +/// described above. +public class AtomFeedEntryAuthor { + + /// The "atom:name" element's content conveys a human-readable name for + /// the person. The content of atom:name is Language-Sensitive. Person + /// constructs MUST contain exactly one "atom:name" element. + public var name: String? + + /// The "atom:email" element's content conveys an e-mail address + /// associated with the person. Person constructs MAY contain an + /// atom:email element, but MUST NOT contain more than one. Its content + /// MUST conform to the "addr-spec" production in [RFC2822]. + public var email: String? + + /// The "atom:uri" element's content conveys an IRI associated with the + /// person. Person constructs MAY contain an atom:uri element, but MUST + /// NOT contain more than one. The content of atom:uri in a Person + /// construct MUST be an IRI reference [RFC3987]. + public var uri: String? + +} + +// MARK: - Equatable + +extension AtomFeedEntryAuthor: Equatable { + + public static func ==(lhs: AtomFeedEntryAuthor, rhs: AtomFeedEntryAuthor) -> Bool { + return + lhs.name == rhs.name && + lhs.email == rhs.email && + lhs.uri == rhs.uri + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryCategory.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryCategory.swift new file mode 100644 index 0000000..2f74ba2 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryCategory.swift @@ -0,0 +1,106 @@ +// +// AtomFeedEntryCategory.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:category" element conveys information about a category +/// associated with an entry or feed. This specification assigns no +/// meaning to the content (if any) of this element. +public class AtomFeedEntryCategory { + + /// The element's attributes + public class Attributes { + + /// The "term" attribute is a string that identifies the category to + /// which the entry or feed belongs. Category elements MUST have a + /// "term" attribute. + public var term: String? + + /// The "scheme" attribute is an IRI that identifies a categorization + /// scheme. Category elements MAY have a "scheme" attribute. + public var scheme: String? + + /// The "label" attribute provides a human-readable label for display in + /// end-user applications. The content of the "label" attribute is + /// Language-Sensitive. Entities such as "&" and "<" represent + /// their corresponding characters ("&" and "<", respectively), not + /// markup. Category elements MAY have a "label" attribute. + public var label: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + +} + +// MARK: - Initializers + +extension AtomFeedEntryCategory { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = AtomFeedEntryCategory.Attributes(attributes: attributeDict) + } + +} + +extension AtomFeedEntryCategory.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.term = attributeDict["term"] + self.scheme = attributeDict["scheme"] + self.label = attributeDict["label"] + + } + +} + +// MARK: - Equatable + +extension AtomFeedEntryCategory: Equatable { + + public static func ==(lhs: AtomFeedEntryCategory, rhs: AtomFeedEntryCategory) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension AtomFeedEntryCategory.Attributes: Equatable { + + public static func ==(lhs: AtomFeedEntryCategory.Attributes, rhs: AtomFeedEntryCategory.Attributes) -> Bool { + return + lhs.term == rhs.term && + lhs.scheme == rhs.scheme && + lhs.label == rhs.label + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryContent.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryContent.swift new file mode 100644 index 0000000..919ff5c --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryContent.swift @@ -0,0 +1,114 @@ +// +// AtomFeedEntryContent.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:content" element either contains or links to the content of +/// the entry. The content of atom:content is Language-Sensitive. +public class AtomFeedEntryContent { + + /// The element's attributes. + public class Attributes { + + /// On the atom:content element, the value of the "type" attribute MAY be + /// one of "text", "html", or "xhtml". Failing that, it MUST conform to + /// the syntax of a MIME media type, but MUST NOT be a composite type + /// (see Section 4.2.6 of [MIMEREG]). If neither the type attribute nor + /// the src attribute is provided, Atom Processors MUST behave as though + /// the type attribute were present with a value of "text". + public var type: String? + + /// The atom:content MAY have a "src" attribute, whose value MUST be an IRI + /// reference [RFC3987]. If the "src" attribute is present, atom:content + /// MUST be empty. Atom Processors MAY use the IRI to retrieve the + /// content and MAY choose to ignore remote content or to present it in a + /// different manner than local content. + /// + /// If the "src" attribute is present, the "type" attribute SHOULD be + /// provided and MUST be a MIME media type [MIMEREG], rather than "text", + /// "html", or "xhtml". The value is advisory; that is to say, when the + /// corresponding URI (mapped from an IRI, if necessary) is dereferenced, + /// if the server providing that content also provides a media type, the + /// server-provided media type is authoritative. + public var src: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension AtomFeedEntryContent { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = AtomFeedEntryContent.Attributes(attributes: attributeDict) + } + +} + +extension AtomFeedEntryContent.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + self.src = attributeDict["src"] + + } + +} + +// MARK: - Equatable + +extension AtomFeedEntryContent: Equatable { + + public static func ==(lhs: AtomFeedEntryContent, rhs: AtomFeedEntryContent) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension AtomFeedEntryContent.Attributes: Equatable { + + public static func ==(lhs: AtomFeedEntryContent.Attributes, rhs: AtomFeedEntryContent.Attributes) -> Bool { + return + lhs.type == rhs.type && + lhs.src == rhs.src + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryContributor.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryContributor.swift new file mode 100644 index 0000000..db71845 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryContributor.swift @@ -0,0 +1,61 @@ +// +// AtomFeedEntryContributor.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:contributor" element is a Person construct that indicates a +/// person or other entity who contributed to the entry or feed. +public class AtomFeedEntryContributor { + + /// The "atom:name" element's content conveys a human-readable name for + /// the person. The content of atom:name is Language-Sensitive. Person + /// constructs MUST contain exactly one "atom:name" element. + public var name: String? + + /// The "atom:email" element's content conveys an e-mail address + /// associated with the person. Person constructs MAY contain an + /// atom:email element, but MUST NOT contain more than one. Its content + /// MUST conform to the "addr-spec" production in [RFC2822]. + public var email: String? + + /// The "atom:uri" element's content conveys an IRI associated with the + /// person. Person constructs MAY contain an atom:uri element, but MUST + /// NOT contain more than one. The content of atom:uri in a Person + /// construct MUST be an IRI reference [RFC3987]. + public var uri: String? + +} + +// MARK: - Equatable + +extension AtomFeedEntryContributor: Equatable { + + public static func ==(lhs: AtomFeedEntryContributor, rhs: AtomFeedEntryContributor) -> Bool { + return + lhs.name == rhs.name && + lhs.email == rhs.email && + lhs.uri == rhs.uri + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryLink.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryLink.swift new file mode 100644 index 0000000..b9f3d5e --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryLink.swift @@ -0,0 +1,183 @@ +// +// AtomFeedEntryLink.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:link" element defines a reference from an entry or feed to +/// a Web resource. This specification assigns no meaning to the content +/// (if any) of this element. +public class AtomFeedEntryLink { + + /// The element's attributes + public class Attributes { + + /// The "href" attribute contains the link's IRI. atom:link elements MUST + /// have an href attribute, whose value MUST be a IRI reference + /// [RFC3987]. + public var href: String? + + /// The atom:link elements MAY have a "rel" attribute that indicates the link + /// relation type. If the "rel" attribute is not present, the link + /// element MUST be interpreted as if the link relation type is + /// "alternate". + /// + /// The value of "rel" MUST be a string that is non-empty and matches + /// either the "isegment-nz-nc" or the "IRI" production in [RFC3987]. + /// Note that use of a relative reference other than a simple name is not + /// allowed. If a name is given, implementations MUST consider the link + /// relation type equivalent to the same name registered within the IANA + /// Registry of Link Relations (Section 7), and thus to the IRI that + /// would be obtained by appending the value of the rel attribute to the + /// string "http://www.iana.org/assignments/relation/". The value of + /// "rel" describes the meaning of the link, but does not impose any + /// behavioral requirements on Atom Processors. + /// + /// This document defines five initial values for the Registry of Link + /// Relations: + /// + /// 1. The value "alternate" signifies that the IRI in the value of the + /// href attribute identifies an alternate version of the resource + /// described by the containing element. + /// + /// 2. The value "related" signifies that the IRI in the value of the + /// href attribute identifies a resource related to the resource + /// described by the containing element. For example, the feed for a + /// site that discusses the performance of the search engine at + /// "http://search.example.com" might contain, as a child of + /// atom:feed: + /// + /// + /// + /// An identical link might appear as a child of any atom:entry whose + /// content contains a discussion of that same search engine. + /// + /// 3. The value "self" signifies that the IRI in the value of the href + /// attribute identifies a resource equivalent to the containing + /// element. + /// + /// 4. The value "enclosure" signifies that the IRI in the value of the + /// href attribute identifies a related resource that is potentially + /// large in size and might require special handling. For atom:link + /// elements with rel="enclosure", the length attribute SHOULD be + /// provided. + /// + /// 5. The value "via" signifies that the IRI in the value of the href + /// attribute identifies a resource that is the source of the + /// information provided in the containing element. + public var rel: String? + + /// On the link element, the "type" attribute's value is an advisory + /// media type: it is a hint about the type of the representation that is + /// expected to be returned when the value of the href attribute is + /// dereferenced. Note that the type attribute does not override the + /// actual media type returned with the representation. Link elements + /// MAY have a type attribute, whose value MUST conform to the syntax of + /// a MIME media type [MIMEREG]. + public var type: String? + + /// The "hreflang" attribute's content describes the language of the + /// resource pointed to by the href attribute. When used together with + /// the rel="alternate", it implies a translated version of the entry. + /// Link elements MAY have an hreflang attribute, whose value MUST be a + /// language tag [RFC3066]. + public var hreflang: String? + + /// The "title" attribute conveys human-readable information about the + /// link. The content of the "title" attribute is Language-Sensitive. + /// Entities such as "&" and "<" represent their corresponding + /// characters ("&" and "<", respectively), not markup. Link elements + /// MAY have a title attribute. + public var title: String? + + /// The "length" attribute indicates an advisory length of the linked + /// content in octets; it is a hint about the content length of the + /// representation returned when the IRI in the href attribute is mapped + /// to a URI and dereferenced. Note that the length attribute does not + /// override the actual content length of the representation as reported + /// by the underlying protocol. Link elements MAY have a length + /// attribute. + public var length: Int64? + + } + + /// The element's attributes. + public var attributes: Attributes? + +} + +// MARK: - Initializers + +extension AtomFeedEntryLink { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = AtomFeedEntryLink.Attributes(attributes: attributeDict) + } + +} + +extension AtomFeedEntryLink.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.href = attributeDict["href"] + self.hreflang = attributeDict["hreflang"] + self.type = attributeDict["type"] + self.rel = attributeDict["rel"] + self.title = attributeDict["title"] + self.length = Int64(attributeDict["length"] ?? "") + + } + +} + +// MARK: - Equatable + +extension AtomFeedEntryLink: Equatable { + + public static func ==(lhs: AtomFeedEntryLink, rhs: AtomFeedEntryLink) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension AtomFeedEntryLink.Attributes: Equatable { + + public static func ==(lhs: AtomFeedEntryLink.Attributes, rhs: AtomFeedEntryLink.Attributes) -> Bool { + return + lhs.href == rhs.href && + lhs.hreflang == rhs.hreflang && + lhs.type == rhs.type && + lhs.rel == rhs.rel && + lhs.title == rhs.title && + lhs.length == rhs.length + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntrySource.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntrySource.swift new file mode 100644 index 0000000..9dd1fcf --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntrySource.swift @@ -0,0 +1,73 @@ +// +// AtomFeedEntrySource.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// If an atom:entry is copied from one feed into another feed, then the +/// source atom:feed's metadata (all child elements of atom:feed other +/// than the atom:entry elements) MAY be preserved within the copied +/// entry by adding an atom:source child element, if it is not already +/// present in the entry, and including some or all of the source feed's +/// Metadata elements as the atom:source element's children. Such +/// metadata SHOULD be preserved if the source atom:feed contains any of +/// the child elements atom:author, atom:contributor, atom:rights, or +/// atom:category and those child elements are not present in the source +/// atom:entry. +/// +/// The atom:source element is designed to allow the aggregation of +/// entries from different feeds while retaining information about an +/// entry's source feed. For this reason, Atom Processors that are +/// performing such aggregation SHOULD include at least the required +/// feed-level Metadata elements (atom:id, atom:title, and atom:updated) +/// in the atom:source element. +public class AtomFeedEntrySource { + + /// The "atom:id" element conveys a permanent, universally unique + /// identifier for an entry or feed. + public var id: String? + + /// The "atom:title" element is a Text construct that conveys a human- + /// readable title for an entry or feed. + public var title: String? + + /// The "atom:updated" element is a Date construct indicating the most + /// recent instant in time when an entry or feed was modified in a way + /// the publisher considers significant. Therefore, not all + /// modifications necessarily result in a changed atom:updated value. + public var updated: Date? + +} + +// MARK: - Equatable + +extension AtomFeedEntrySource: Equatable { + + public static func ==(lhs: AtomFeedEntrySource, rhs: AtomFeedEntrySource) -> Bool { + return + lhs.id == rhs.id && + lhs.title == rhs.title && + lhs.updated == rhs.updated + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntrySummary.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntrySummary.swift new file mode 100644 index 0000000..0e8ecf1 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntrySummary.swift @@ -0,0 +1,101 @@ +// +// AtomFeedEntrySummary.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:summary" element is a Text construct that conveys a short +/// summary, abstract, or excerpt of an enactry. +/// +/// atomSummary = element atom:summary { atomTextConstruct } +/// +/// It is not advisable for the atom:summary element to duplicate +/// atom:title or atom:content because Atom Processors might assume there +/// is a useful summary when there is none. +public class AtomFeedEntrySummary { + + /// The element's attributes. + public class Attributes { + + /// Text constructs MAY have a "type" attribute. When present, the value + /// MUST be one of "text", "html", or "xhtml". If the "type" attribute + /// is not provided, Atom Processors MUST behave as though it were + /// present with a value of "text". + public var type: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension AtomFeedEntrySummary { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = AtomFeedEntrySummary.Attributes(attributes: attributeDict) + } + +} + +extension AtomFeedEntrySummary.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + + } + +} + +// MARK: - Equatable + +extension AtomFeedEntrySummary: Equatable { + + public static func ==(lhs: AtomFeedEntrySummary, rhs: AtomFeedEntrySummary) -> Bool { + return + lhs.attributes == rhs.attributes && + lhs.value == rhs.value + } + +} + +extension AtomFeedEntrySummary.Attributes: Equatable { + + public static func ==(lhs: AtomFeedEntrySummary.Attributes, rhs: AtomFeedEntrySummary.Attributes) -> Bool { + return lhs.type == rhs.type + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedGenerator.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedGenerator.swift new file mode 100644 index 0000000..487d55e --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedGenerator.swift @@ -0,0 +1,115 @@ +// +// AtomFeedGenerator.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:generator" element's content identifies the agent used to +/// generate a feed, for debugging and other purposes. +/// +/// The content of this element, when present, MUST be a string that is a +/// human-readable name for the generating agent. Entities such as +/// "&" and "<" represent their corresponding characters ("&" and +/// "<" respectively), not markup. +/// +/// The atom:generator element MAY have a "uri" attribute whose value +/// MUST be an IRI reference [RFC3987]. When dereferenced, the resulting +/// URI (mapped from an IRI, if necessary) SHOULD produce a +/// representation that is relevant to that agent. +/// +/// The atom:generator element MAY have a "version" attribute that +/// indicates the version of the generating agent. +public class AtomFeedGenerator { + + /// The element's attributes. + public class Attributes { + + /// The atom:generator element MAY have a "uri" attribute whose value + /// MUST be an IRI reference [RFC3987]. When dereferenced, the resulting + /// URI (mapped from an IRI, if necessary) SHOULD produce a + /// representation that is relevant to that agent. + public var uri: String? + + /// The atom:generator element MAY have a "version" attribute that + /// indicates the version of the generating agent. + public var version: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension AtomFeedGenerator { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = AtomFeedGenerator.Attributes(attributes: attributeDict) + } + +} + +extension AtomFeedGenerator.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.uri = attributeDict["uri"] + self.version = attributeDict["version"] + + } + +} + +// MARK: - Equatable + +extension AtomFeedGenerator: Equatable { + + public static func ==(lhs: AtomFeedGenerator, rhs: AtomFeedGenerator) -> Bool { + return + lhs.attributes == rhs.attributes && + lhs.value == rhs.value + } + +} + +extension AtomFeedGenerator.Attributes: Equatable { + + public static func ==(lhs: AtomFeedGenerator.Attributes, rhs: AtomFeedGenerator.Attributes) -> Bool { + return + lhs.uri == rhs.uri && + lhs.version == rhs.version + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedLink.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedLink.swift new file mode 100644 index 0000000..05f6b52 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedLink.swift @@ -0,0 +1,183 @@ +// +// AtomFeedLink.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:link" element defines a reference from an entry or feed to +/// a Web resource. This specification assigns no meaning to the content +/// (if any) of this element. +public class AtomFeedLink { + + /// The element's attributes. + public class Attributes { + + /// The "href" attribute contains the link's IRI. atom:link elements MUST + /// have an href attribute, whose value MUST be a IRI reference + /// [RFC3987]. + public var href: String? + + /// The atom:link elements MAY have a "rel" attribute that indicates the link + /// relation type. If the "rel" attribute is not present, the link + /// element MUST be interpreted as if the link relation type is + /// "alternate". + /// + /// The value of "rel" MUST be a string that is non-empty and matches + /// either the "isegment-nz-nc" or the "IRI" production in [RFC3987]. + /// Note that use of a relative reference other than a simple name is not + /// allowed. If a name is given, implementations MUST consider the link + /// relation type equivalent to the same name registered within the IANA + /// Registry of Link Relations (Section 7), and thus to the IRI that + /// would be obtained by appending the value of the rel attribute to the + /// string "http://www.iana.org/assignments/relation/". The value of + /// "rel" describes the meaning of the link, but does not impose any + /// behavioral requirements on Atom Processors. + /// + /// This document defines five initial values for the Registry of Link + /// Relations: + /// + /// 1. The value "alternate" signifies that the IRI in the value of the + /// href attribute identifies an alternate version of the resource + /// described by the containing element. + /// + /// 2. The value "related" signifies that the IRI in the value of the + /// href attribute identifies a resource related to the resource + /// described by the containing element. For example, the feed for a + /// site that discusses the performance of the search engine at + /// "http://search.example.com" might contain, as a child of + /// atom:feed: + /// + /// + /// + /// An identical link might appear as a child of any atom:entry whose + /// content contains a discussion of that same search engine. + /// + /// 3. The value "self" signifies that the IRI in the value of the href + /// attribute identifies a resource equivalent to the containing + /// element. + /// + /// 4. The value "enclosure" signifies that the IRI in the value of the + /// href attribute identifies a related resource that is potentially + /// large in size and might require special handling. For atom:link + /// elements with rel="enclosure", the length attribute SHOULD be + /// provided. + /// + /// 5. The value "via" signifies that the IRI in the value of the href + /// attribute identifies a resource that is the source of the + /// information provided in the containing element. + public var rel: String? + + /// On the link element, the "type" attribute's value is an advisory + /// media type: it is a hint about the type of the representation that is + /// expected to be returned when the value of the href attribute is + /// dereferenced. Note that the type attribute does not override the + /// actual media type returned with the representation. Link elements + /// MAY have a type attribute, whose value MUST conform to the syntax of + /// a MIME media type [MIMEREG]. + public var type: String? + + /// The "hreflang" attribute's content describes the language of the + /// resource pointed to by the href attribute. When used together with + /// the rel="alternate", it implies a translated version of the entry. + /// Link elements MAY have an hreflang attribute, whose value MUST be a + /// language tag [RFC3066]. + public var hreflang: String? + + /// The "title" attribute conveys human-readable information about the + /// link. The content of the "title" attribute is Language-Sensitive. + /// Entities such as "&" and "<" represent their corresponding + /// characters ("&" and "<", respectively), not markup. Link elements + /// MAY have a title attribute. + public var title: String? + + /// The "length" attribute indicates an advisory length of the linked + /// content in octets; it is a hint about the content length of the + /// representation returned when the IRI in the href attribute is mapped + /// to a URI and dereferenced. Note that the length attribute does not + /// override the actual content length of the representation as reported + /// by the underlying protocol. Link elements MAY have a length + /// attribute. + public var length: Int64? + + } + + /// The element's attributes. + public var attributes: Attributes? + +} + +// MARK: - Initializers + +extension AtomFeedLink { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = AtomFeedLink.Attributes(attributes: attributeDict) + } + +} + +extension AtomFeedLink.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.href = attributeDict["href"] + self.hreflang = attributeDict["hreflang"] + self.type = attributeDict["type"] + self.rel = attributeDict["rel"] + self.title = attributeDict["title"] + self.length = Int64(attributeDict["length"] ?? "") + + } + +} + +// MARK: - Equatable + +extension AtomFeedLink: Equatable { + + public static func ==(lhs: AtomFeedLink, rhs: AtomFeedLink) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension AtomFeedLink.Attributes: Equatable { + + public static func ==(lhs: AtomFeedLink.Attributes, rhs: AtomFeedLink.Attributes) -> Bool { + return + lhs.href == rhs.href && + lhs.hreflang == rhs.hreflang && + lhs.type == rhs.type && + lhs.rel == rhs.rel && + lhs.title == rhs.title && + lhs.length == rhs.length + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedSubtitle.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedSubtitle.swift new file mode 100644 index 0000000..2c9013a --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedSubtitle.swift @@ -0,0 +1,95 @@ +// +// AtomFeedSubtitle.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:subtitle" element is a Text construct that conveys a human- +/// readable description or subtitle for a feed. +public class AtomFeedSubtitle { + + /// The element's attributes. + public class Attributes { + + /// Text constructs MAY have a "type" attribute. When present, the value + /// MUST be one of "text", "html", or "xhtml". If the "type" attribute + /// is not provided, Atom Processors MUST behave as though it were + /// present with a value of "text". + public var type: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension AtomFeedSubtitle { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = AtomFeedSubtitle.Attributes(attributes: attributeDict) + } + +} + +extension AtomFeedSubtitle.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + + } + +} + +// MARK: - Equatable + +extension AtomFeedSubtitle: Equatable { + + public static func ==(lhs: AtomFeedSubtitle, rhs: AtomFeedSubtitle) -> Bool { + return + lhs.attributes == rhs.attributes && + lhs.value == rhs.value + } + +} + +extension AtomFeedSubtitle.Attributes: Equatable { + + public static func ==(lhs: AtomFeedSubtitle.Attributes, rhs: AtomFeedSubtitle.Attributes) -> Bool { + return lhs.type == rhs.type + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomPath.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomPath.swift new file mode 100644 index 0000000..3a9f7b2 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomPath.swift @@ -0,0 +1,110 @@ +// +// AtomPath.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Describes the individual path for each XML DOM element of an Atom feed. +/// +/// See https://tools.ietf.org/html/rfc4287 +enum AtomPath: String { + + case feed = "/feed" + case feedTitle = "/feed/title" + case feedSubtitle = "/feed/subtitle" + case feedLink = "/feed/link" + case feedUpdated = "/feed/updated" + case feedCategory = "/feed/category" + case feedAuthor = "/feed/author" + case feedAuthorName = "/feed/author/name" + case feedAuthorEmail = "/feed/author/email" + case feedAuthorUri = "/feed/author/uri" + case feedContributor = "/feed/contributor" + case feedContributorName = "/feed/contributor/name" + case feedContributorEmail = "/feed/contributor/email" + case feedContributorUri = "/feed/contributor/uri" + case feedID = "/feed/id" + case feedGenerator = "/feed/generator" + case feedIcon = "/feed/icon" + case feedLogo = "/feed/logo" + case feedRights = "/feed/rights" + case feedEntry = "/feed/entry" + case feedEntryTitle = "/feed/entry/title" + case feedEntrySummary = "/feed/entry/summary" + case feedEntryLink = "/feed/entry/link" + case feedEntryUpdated = "/feed/entry/updated" + case feedEntryCategory = "/feed/entry/category" + case feedEntryID = "/feed/entry/id" + case feedEntryContent = "/feed/entry/content" + case feedEntryPublished = "/feed/entry/published" + case feedEntrySource = "/feed/entry/source" + case feedEntrySourceID = "/feed/entry/source/id" + case feedEntrySourceTitle = "/feed/entry/source/title" + case feedEntrySourceUpdated = "/feed/entry/source/updated" + case feedEntryRights = "/feed/entry/rights" + case feedEntryAuthor = "/feed/entry/author" + case feedEntryAuthorName = "/feed/entry/author/name" + case feedEntryAuthorEmail = "/feed/entry/author/email" + case feedEntryAuthorUri = "/feed/entry/author/uri" + case feedEntryContributor = "/feed/entry/contributor" + case feedEntryContributorName = "/feed/entry/contributor/name" + case feedEntryContributorEmail = "/feed/entry/contributor/email" + case feedEntryContributorUri = "/feed/entry/contributor/uri" + + // MARK: Media + + case feedEntryMediaThumbnail = "/feed/entry/media:thumbnail" + case feedEntryMediaContent = "/feed/entry/media:content" + case feedEntryMediaCommunity = "/feed/entry/media:community" + case feedEntryMediaCommunityMediaStarRating = "/feed/entry/media:community/media:starRating" + case feedEntryMediaCommunityMediaStatistics = "/feed/entry/media:community/media:statistics" + case feedEntryMediaCommunityMediaTags = "/feed/entry/media:community/media:tags" + case feedEntryMediaComments = "/feed/entry/media:comments" + case feedEntryMediaCommentsMediaComment = "/feed/entry/media:comments/media:comment" + case feedEntryMediaEmbed = "/feed/entry/media:embed" + case feedEntryMediaEmbedMediaParam = "/feed/entry/media:embed/media:param" + case feedEntryMediaResponses = "/feed/entry/media:responses" + case feedEntryMediaResponsesMediaResponse = "/feed/entry/media:responses/media:response" + case feedEntryMediaBackLinks = "/feed/entry/media:backLinks" + case feedEntryMediaBackLinksBackLink = "/feed/entry/media:backLinks/media:backLink" + case feedEntryMediaStatus = "/feed/entry/media:status" + case feedEntryMediaPrice = "/feed/entry/media:price" + case feedEntryMediaLicense = "/feed/entry/media:license" + case feedEntryMediaSubTitle = "/feed/entry/media:subTitle" + case feedEntryMediaPeerLink = "/feed/entry/media:peerLink" + case feedEntryMediaLocation = "/feed/entry/media:location" + case feedEntryMediaLocationPosition = "/feed/entry/media:location/georss:where/gml:Point/gml:pos" + case feedEntryMediaRestriction = "/feed/entry/media:restriction" + case feedEntryMediaScenes = "/feed/entry/media:scenes" + case feedEntryMediaScenesMediaScene = "/feed/entry/media:scenes/media:scene" + case feedEntryMediaScenesMediaSceneSceneTitle = "/feed/entry/media:scenes/media:scene/sceneTitle" + case feedEntryMediaScenesMediaSceneSceneDescription = "/feed/entry/media:scenes/media:scene/sceneDescription" + case feedEntryMediaScenesMediaSceneSceneStartTime = "/feed/entry/media:scenes/media:scene/sceneStartTime" + case feedEntryMediaScenesMediaSceneSceneEndTime = "/feed/entry/media:scenes/media:scene/sceneEndTime" + case feedEntryMediaGroup = "/feed/entry/media:group" + case feedEntryMediaGroupMediaCredit = "/feed/entry/media:group/media:credit" + case feedEntryMediaGroupMediaCategory = "/feed/entry/media:group/media:category" + case feedEntryMediaGroupMediaRating = "/feed/entry/media:group/media:rating" + case feedEntryMediaGroupMediaContent = "/feed/entry/media:group/media:content" + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeed.swift b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeed.swift new file mode 100644 index 0000000..b77000b --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeed.swift @@ -0,0 +1,190 @@ +// +// JSONFeed.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The JSON Feed format is a pragmatic syndication format, like RSS and Atom, +/// but with one big difference: it's JSON instead of XML. +/// See https://jsonfeed.org/version/1 +public class JSONFeed { + + /// (required, string) is the URL of the version of the format the feed + /// uses. This should appear at the very top, though we recognize that not all + /// JSON generators allow for ordering. + public var version: String? + + /// (required, string) is the name of the feed, which will often correspond to + /// the name of the website (blog, for instance), though not necessarily. + public var title: String? + + /// (optional but strongly recommended, string) is the URL of the resource that + /// the feed describes. This resource may or may not actually be a "home" page, + /// but it should be an HTML page. If a feed is published on the public web, + /// this should be considered as required. But it may not make sense in the + /// case of a file created on a desktop computer, when that file is not shared + /// or is shared only privately. + public var homePageURL: String? + + /// (optional but strongly recommended, string) is the URL of the feed, and + /// serves as the unique identifier for the feed. As with home_page_url, this + /// should be considered required for feeds on the public web. + public var feedUrl: String? + + /// (optional, string) provides more detail, beyond the title, on what the feed + /// is about. A feed reader may display this text. + public var description: String? + + /// (optional, string) is a description of the purpose of the feed. This is for + /// the use of people looking at the raw JSON, and should be ignored by feed + /// readers. + public var userComment: String? + + /// (optional, string) is the URL of a feed that provides the next n items, + /// where n is determined by the publisher. This allows for pagination, but + /// with the expectation that reader software is not required to use it and + /// probably won't use it very often. next_url must not be the same as + /// feed_url, and it must not be the same as a previous next_url (to avoid + /// infinite loops). + public var nextUrl: String? + + /// (optional, string) is the URL of an image for the feed suitable to be used + /// in a timeline, much the way an avatar might be used. It should be square + /// and relatively large - such as 512 x 512 - so that it can be scaled-down + /// and so that it can look good on retina displays. It should use transparency + /// where appropriate, since it may be rendered on a non-white background. + public var icon: String? + + /// (optional, string) is the URL of an image for the feed suitable to be used + /// in a source list. It should be square and relatively small, but not smaller + /// than 64 x 64 (so that it can look good on retina displays). As with icon, + /// this image should use transparency where appropriate, since it may be + /// rendered on a non-white background. + public var favicon: String? + + /// (optional, object) specifies the feed author. The author object has + /// several members. These are all optional - but if you provide an author + /// object, then at least one is required. + public var author: JSONFeedAuthor? + + /// (optional, boolean) says whether or not the feed is finished - that is, + /// whether or not it will ever update again. A feed for a temporary event, + /// such as an instance of the Olympics, could expire. If the value is true, + /// then it's expired. Any other value, or the absence of expired, means the + /// feed may continue to update. + public var expired: Bool? + + /// (very optional, array of objects) describes endpoints that can be used to + /// subscribe to real-time notifications from the publisher of this feed. Each + /// object has a type and url, both of which are required. + public var hubs: [JSONFeedHub]? + + /// The JSONFeed items. + public var items: [JSONFeedItem]? + + /// Publisher's custom objects. + /// + /// If you find the need to use these extensions please do so as a temporary + /// solution and open an issue on github so that direct support can be added + /// through a strongly typed model. + public var extensions: [String: Any?]? + +} + +// MARK: - Initializers + +extension JSONFeed { + + convenience init?(dictionary: [String : Any?]) { + + if dictionary.isEmpty { + return nil + } + + self.init() + + self.version = dictionary["version"] as? String + self.title = dictionary["title"] as? String + self.userComment = dictionary["user_comment"] as? String + self.homePageURL = dictionary["home_page_url"] as? String + self.description = dictionary["description"] as? String + self.feedUrl = dictionary["feed_url"] as? String + self.nextUrl = dictionary["next_url"] as? String + self.icon = dictionary["icon"] as? String + self.favicon = dictionary["favicon"] as? String + self.expired = dictionary["expired"] as? Bool + + if let items = dictionary["items"] as? [[String: Any?]] { + self.items = items.flatMap({ (item) -> JSONFeedItem? in + return JSONFeedItem(dictionary: item) + }) + } + + if let authorDictionary = dictionary["author"] as? [String: Any] { + self.author = JSONFeedAuthor(dictionary: authorDictionary) + } + + if let hubs = dictionary["hubs"] as? [[String: Any?]] { + self.hubs = hubs.flatMap({ (hub) -> JSONFeedHub? in + return JSONFeedHub(dictionary: hub) + }) + } + + let privateExtensionKeys = dictionary.keys.flatMap { (key) -> String? in + return key.hasPrefix("_") ? key : nil + } + + if !privateExtensionKeys.isEmpty { + extensions = [:] + privateExtensionKeys.forEach { (key) in + extensions?[key] = dictionary[key] + } + } + + } + +} + +// MARK: - Equatable + +extension JSONFeed: Equatable { + + public static func ==(lhs: JSONFeed, rhs: JSONFeed) -> Bool { + return + lhs.title == rhs.title && + lhs.version == rhs.version && + lhs.title == rhs.title && + lhs.userComment == rhs.userComment && + lhs.homePageURL == rhs.homePageURL && + lhs.description == rhs.description && + lhs.feedUrl == rhs.feedUrl && + lhs.nextUrl == rhs.nextUrl && + lhs.icon == rhs.icon && + lhs.favicon == rhs.favicon && + lhs.expired == rhs.expired && + lhs.items == rhs.items && + lhs.author == rhs.author && + lhs.hubs == rhs.hubs + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedAttachment.swift b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedAttachment.swift new file mode 100644 index 0000000..1ec1418 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedAttachment.swift @@ -0,0 +1,88 @@ +// +// JSONFeedAttachment.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Describes optional attatchments of a JSON Feed item. +public class JSONFeedAttachment { + + /// (required, string) specifies the location of the attachment. + public var url: String? + + /// (required, string) specifies the type of the attachment, such as + /// "audio/mpeg." + public var mimeType: String? + + /// (optional, string) is a name for the attachment. Important: if there are + /// multiple attachments, and two or more have the exact same title (when title + /// is present), then they are considered as alternate representations of the + /// same thing. In this way a podcaster, for instance, might provide an audio + /// recording in different formats. + public var title: String? + + /// (optional, number) specifies how large the file is. + public var sizeInBytes: Int? + + /// (optional, number) specifies how long it takes to listen to or watch, when + /// played at normal speed. + public var durationInSeconds: TimeInterval? + +} + +// MARK: - Initializers + +extension JSONFeedAttachment { + + convenience init?(dictionary: [String : Any?]) { + + if dictionary.isEmpty { + return nil + } + + self.init() + + self.title = dictionary["title"] as? String + self.url = dictionary["url"] as? String + self.mimeType = dictionary["mime_type"] as? String + self.sizeInBytes = dictionary["size_in_bytes"] as? Int + self.durationInSeconds = dictionary["duration_in_seconds"] as? TimeInterval + + } + +} + +// MARK: - Equatable + +extension JSONFeedAttachment: Equatable { + + public static func ==(lhs: JSONFeedAttachment, rhs: JSONFeedAttachment) -> Bool { + return + lhs.title == rhs.title && + lhs.url == rhs.url && + lhs.mimeType == rhs.mimeType && + lhs.sizeInBytes == rhs.sizeInBytes && + lhs.durationInSeconds == rhs.durationInSeconds + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedAuthor.swift b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedAuthor.swift new file mode 100644 index 0000000..85c2f17 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedAuthor.swift @@ -0,0 +1,80 @@ +// +// JSONFeedAuthor.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// (optional, object) specifies the feed author. The author object has several +/// members. These are all optional - but if you provide an author object, then at +/// least one is required: +public class JSONFeedAuthor { + + /// (optional, string) is the author's name. + public var name: String? + + /// (optional, string) is the URL of a site owned by the author. It could be a + /// blog, micro-blog, Twitter account, and so on. Ideally the linked-to page + /// provides a way to contact the author, but that's not required. The URL + /// could be a mailto: link, though we suspect that will be rare. + public var url: String? + + /// (optional, string) is the URL for an image for the author. As with icon, + /// it should be square and relatively large - such as 512 x 512 - and should + /// use transparency where appropriate, since it may be rendered on a non-white + /// background. + public var avatar: String? + +} + +// MARK: - Initializers + +extension JSONFeedAuthor { + + convenience init?(dictionary: [String : Any?]) { + + if dictionary.isEmpty { + return nil + } + + self.init() + + self.name = dictionary["name"] as? String + self.url = dictionary["url"] as? String + self.avatar = dictionary["avatar"] as? String + + } + +} + +// MARK: - Equatable + +extension JSONFeedAuthor: Equatable { + + public static func ==(lhs: JSONFeedAuthor, rhs: JSONFeedAuthor) -> Bool { + return + lhs.name == rhs.name && + lhs.url == rhs.url && + lhs.avatar == rhs.avatar + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedHub.swift b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedHub.swift new file mode 100644 index 0000000..95b08e9 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedHub.swift @@ -0,0 +1,69 @@ +// +// JSONFeedHub.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Describes an endpoints that can be used to subscribe to real-time notifications +/// from the publisher of this feed. Each object has a type and url, both of which +/// are required. +public class JSONFeedHub { + + /// The protocol used to talk with the hub, such as "rssCloud" or "WebSub." + public var type: String? + + /// The hub's url. + public var url: String? + +} + +// MARK: - Initializers + +extension JSONFeedHub { + + convenience init?(dictionary: [String : Any?]) { + + if dictionary.isEmpty { + return nil + } + + self.init() + + self.type = dictionary["type"] as? String + self.url = dictionary["url"] as? String + + } + +} + +// MARK: - Equatable + +extension JSONFeedHub: Equatable { + + public static func ==(lhs: JSONFeedHub, rhs: JSONFeedHub) -> Bool { + return + lhs.type == rhs.type && + lhs.url == rhs.url + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedItem.swift b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedItem.swift new file mode 100644 index 0000000..78ae197 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedItem.swift @@ -0,0 +1,189 @@ +// +// JSONFeedItem.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// An individual item of a JSON Feed, acting as a container for metadata and data +/// associated with the item. +public class JSONFeedItem { + + /// (required, string) is unique for that item for that feed over time. If an + /// item is ever updated, the id should be unchanged. New items should never + /// use a previously-used id. If an id is presented as a number or other type, + /// a JSON Feed reader must coerce it to a string. Ideally, the id is the full + /// URL of the resource described by the item, since URLs make great unique + /// identifiers. + public var id: String? + + /// (optional, string) is the URL of the resource described by the item. It's + /// the permalink. This may be the same as the id - but should be present + /// regardless. + public var url: String? + + /// (very optional, string) is the URL of a page elsewhere. This is especially + /// useful for linkblogs. If url links to where you're talking about a thing, + /// then external_url links to the thing you're talking about. + public var externalUrl: String? + + /// (optional, string) is plain text. Microblog items in particular may omit + /// titles. + public var title: String? + + /// content_html and content_text are each optional strings - but one or both + /// must be present. This is the HTML or plain text of the item. Important: + /// the only place HTML is allowed in this format is in content_html. A + /// Twitter-like service might use content_text, while a blog might use + /// content_html. Use whichever makes sense for your resource. (It doesn't + /// even have to be the same for each item in a feed.) + public var contentText: String? + + /// content_html and content_text are each optional strings - but one or both + /// must be present. This is the HTML or plain text of the item. Important: + /// the only place HTML is allowed in this format is in content_html. A + /// Twitter-like service might use content_text, while a blog might use + /// content_html. Use whichever makes sense for your resource. (It doesn't + /// even have to be the same for each item in a feed.) + public var contentHtml: String? + + /// (optional, string) is a plain text sentence or two describing the item. + /// This might be presented in a timeline, for instance, where a detail view + /// would display all of content_html or content_text. + public var summary: String? + + /// (optional, string) is the URL of the main image for the item. This image + /// may also appear in the content_html - if so, it's a hint to the feed reader + /// that this is the main, featured image. Feed readers may use the image as a + /// preview (probably resized as a thumbnail and placed in a timeline). + public var image: String? + + /// (optional, string) is the URL of an image to use as a banner. Some blogging + /// systems (such as Medium) display a different banner image chosen to go with + /// each post, but that image wouldn't otherwise appear in the content_html. + /// A feed reader with a detail view may choose to show this banner image at + /// the top of the detail view, possibly with the title overlaid. + public var bannerImage: String? + + /// (optional, string) specifies the date in RFC 3339 format. + /// (Example: 2010-02-07T14:04:00-05:00.) + public var datePublished: Date? + + /// (optional, string) specifies the modification date in RFC 3339 format. + public var dateModified: Date? + + /// (optional, object) has the same structure as the top-level author. + /// If not specified in an item, then the top-level author, if present, is the + /// author of the item. + public var author: JSONFeedAuthor? + + /// (optional, array of strings) can have any plain text values you want. Tags + /// tend to be just one word, but they may be anything. Note: they are not the + /// equivalent of Twitter hashtags. Some blogging systems and other feed + /// formats call these categories. + public var tags: [String]? + + /// (optional, array) lists related resources. + public var attachments: [JSONFeedAttachment]? + + /// Publisher's custom objects. + /// + /// If you find the need to use these extensions please do so as a temporary + /// solution and open an issue on github so that direct support can be added + /// through a strongly typed model. + public var extensions: [String: Any?]? + +} + +// MARK: - Initializers + +extension JSONFeedItem { + + convenience init?(dictionary: [String : Any?]) { + + if dictionary.isEmpty { + return nil + } + + self.init() + + self.id = dictionary["id"] as? String + self.title = dictionary["title"] as? String + self.url = dictionary["url"] as? String + self.externalUrl = dictionary["external_url"] as? String + self.contentText = dictionary["content_text"] as? String + self.contentHtml = dictionary["content_html"] as? String + self.summary = dictionary["summary"] as? String + self.image = dictionary["image"] as? String + self.bannerImage = dictionary["banner_image"] as? String + self.datePublished = (dictionary["date_published"] as? String)?.toDate(from: .rfc3999) + self.dateModified = (dictionary["date_modified"] as? String)?.toDate(from: .rfc3999) + self.tags = dictionary["tags"] as? [String] + + if let authorDictionary = dictionary["author"] as? [String: Any] { + self.author = JSONFeedAuthor(dictionary: authorDictionary) + } + + if let attachments = dictionary["attachments"] as? [[String: Any?]] { + self.attachments = attachments.flatMap({ (attachment) -> JSONFeedAttachment? in + return JSONFeedAttachment(dictionary: attachment) + }) + } + + let privateExtensionKeys = dictionary.keys.flatMap { (key) -> String? in + return key.hasPrefix("_") ? key : nil + } + + if !privateExtensionKeys.isEmpty { + extensions = [:] + privateExtensionKeys.forEach { (key) in + extensions?[key] = dictionary[key] + } + } + + } + +} + +// MARK: - Equatable + +extension JSONFeedItem: Equatable { + + public static func ==(lhs: JSONFeedItem, rhs: JSONFeedItem) -> Bool { + return + lhs.id == rhs.id && + lhs.title == rhs.title && + lhs.url == rhs.url && + lhs.externalUrl == rhs.externalUrl && + lhs.contentText == rhs.contentText && + lhs.contentHtml == rhs.contentHtml && + lhs.summary == rhs.summary && + lhs.image == rhs.image && + lhs.bannerImage == rhs.bannerImage && + lhs.datePublished == rhs.datePublished && + lhs.dateModified == rhs.dateModified && + lhs.tags == rhs.tags && + lhs.author == rhs.author && + lhs.attachments == rhs.attachments + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Content/ContentNamespace.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Content/ContentNamespace.swift new file mode 100755 index 0000000..e4d52c6 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Content/ContentNamespace.swift @@ -0,0 +1,49 @@ +// +// ContentNamespace.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// A module for the actual content of websites, in multiple formats. +/// See http://web.resource.org/rss/1.0/modules/content/ +public class ContentNamespace { + + /// An element whose contents are the entity-encoded or CDATA-escaped version + /// of the content of the item. + /// + /// Example: + /// What a beautiful day!

]]> + ///
+ public var contentEncoded: String? + +} + +// MARK: - Equatable + +extension ContentNamespace: Equatable { + + public static func ==(lhs: ContentNamespace, rhs: ContentNamespace) -> Bool { + return lhs.contentEncoded == rhs.contentEncoded + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Dublin Core/DublinCoreNamespace.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Dublin Core/DublinCoreNamespace.swift new file mode 100755 index 0000000..4fc92fe --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Dublin Core/DublinCoreNamespace.swift @@ -0,0 +1,163 @@ +// +// DublinCoreNamespace.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The Dublin Core Metadata Element Set is a standard for cross-domain +/// resource description. +/// +/// See https://tools.ietf.org/html/rfc5013 +public class DublinCoreNamespace { + + /// A name given to the resource. + public var dcTitle: String? + + /// An entity primarily responsible for making the content of the resource + /// + /// Examples of a Creator include a person, an organization, or a service. + /// Typically, the name of a Creator should be used to indicate the entity. + public var dcCreator: String? + + /// The topic of the content of the resource + /// + /// Typically, the subject will be represented using keywords, key phrases, + /// or classification codes. Recommended best practice is to use a controlled + /// vocabulary. To describe the spatial or temporal topic of the resource, + /// use the Coverage element. + public var dcSubject: String? + + /// An account of the content of the resource + /// + /// Description may include but is not limited to: an abstract, a table of + /// contents, a graphical representation, or a free-text account of the + /// resource. + public var dcDescription: String? + + /// An entity responsible for making the resource available + /// + /// Examples of a Publisher include a person, an organization, or a service. + /// Typically, the name of a Publisher should be used to indicate the entity. + public var dcPublisher: String? + + /// An entity responsible for making contributions to the content of the + /// resource + /// + /// Examples of a Contributor include a person, an organization, or a service. + /// Typically, the name of a Contributor should be used to indicate the entity. + public var dcContributor: String? + + /// A point or period of time associated with an event in the lifecycle of the + /// resource. + /// + /// Date may be used to express temporal information at any level of + /// granularity. Recommended best practice is to use an encoding scheme, such + /// as the W3CDTF profile of ISO 8601 [W3CDTF]. + public var dcDate: Date? + + /// The nature or genre of the content of the resource + /// + /// Recommended best practice is to use a controlled vocabulary such as the + /// DCMI Type Vocabulary [DCTYPE]. To describe the file format, physical + /// medium, or dimensions of the resource, use the Format element. + public var dcType: String? + + /// The file format, physical medium, or dimensions of the resource. + /// + /// Examples of dimensions include size and duration. Recommended best + /// practice is to use a controlled vocabulary such as the list of Internet + /// Media Types [MIME]. + public var dcFormat: String? + + /// An unambiguous reference to the resource within a given context. + /// + /// Recommended best practice is to identify the resource by means of a string + /// conforming to a formal identification system. + public var dcIdentifier: String? + + /// A Reference to a resource from which the present resource is derived + /// + /// The described resource may be derived from the related resource in whole + /// or in part. Recommended best practice is to identify the related resource + /// by means of a string conforming to a formal identification system. + public var dcSource: String? + + /// A language of the resource. + /// + /// Recommended best practice is to use a controlled vocabulary such as + /// RFC 4646 [RFC4646]. + public var dcLanguage: String? + + /// A related resource. + /// + /// Recommended best practice is to identify the related resource by means of + /// a string conforming to a formal identification system. + public var dcRelation: String? + + /// The spatial or temporal topic of the resource, the spatial applicability + /// of the resource, or the jurisdiction under which the resource is + /// relevant. + /// + /// Spatial topic and spatial applicability may be a named place or a location + /// specified by its geographic coordinates. Temporal topic may be a named + /// period, date, or date range. A jurisdiction may be a named administrative + /// entity or a geographic place to which the resource applies. Recommended + /// best practice is to use a controlled vocabulary such as the Thesaurus of + /// Geographic Names [TGN]. Where appropriate, named places or time periods + /// can be used in preference to numeric identifiers such as sets of + /// coordinates or date ranges. + public var dcCoverage: String? + + /// Information about rights held in and over the resource. + /// + /// Typically, rights information includes a statement about various property + /// rights associated with the resource, including intellectual property + /// rights. + public var dcRights: String? + +} + +// MARK: - Equatable + +extension DublinCoreNamespace: Equatable { + + public static func ==(lhs: DublinCoreNamespace, rhs: DublinCoreNamespace) -> Bool { + return + lhs.dcTitle == rhs.dcTitle && + lhs.dcCreator == rhs.dcCreator && + lhs.dcSubject == rhs.dcSubject && + lhs.dcDescription == rhs.dcDescription && + lhs.dcPublisher == rhs.dcPublisher && + lhs.dcContributor == rhs.dcContributor && + lhs.dcDate == rhs.dcDate && + lhs.dcType == rhs.dcType && + lhs.dcFormat == rhs.dcFormat && + lhs.dcIdentifier == rhs.dcIdentifier && + lhs.dcSource == rhs.dcSource && + lhs.dcLanguage == rhs.dcLanguage && + lhs.dcRelation == rhs.dcRelation && + lhs.dcCoverage == rhs.dcCoverage && + lhs.dcRights == rhs.dcRights + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCategory.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCategory.swift new file mode 100644 index 0000000..394947b --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCategory.swift @@ -0,0 +1,102 @@ +// +// MediaCategory.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Allows a taxonomy to be set that gives an indication of the type of media +/// content, and its particular contents. It has two optional attributes. +public class MediaCategory { + + /// The element's attributes. + public class Attributes { + + /// The URI that identifies the categorization scheme. It is an optional + /// attribute. If this attribute is not included, the default scheme + /// is "http://search.yahoo.com/mrss/category_schema". + public var scheme: String? + + /// The human readable label that can be displayed in end user + /// applications. It is an optional attribute. + public var label: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension MediaCategory { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaCategory.Attributes(attributes: attributeDict) + } + +} + + +extension MediaCategory.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.scheme = attributeDict["scheme"] + self.label = attributeDict["label"] + + } + +} + +// MARK: - Equatable + +extension MediaCategory: Equatable { + + public static func ==(lhs: MediaCategory, rhs: MediaCategory) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaCategory.Attributes: Equatable { + + public static func ==(lhs: MediaCategory.Attributes, rhs: MediaCategory.Attributes) -> Bool { + return + lhs.scheme == rhs.scheme && + lhs.label == rhs.label + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCommunity.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCommunity.swift new file mode 100644 index 0000000..b351021 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCommunity.swift @@ -0,0 +1,60 @@ +// +// MediaCommunity.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// This element stands for the community related content. This allows +/// inclusion of the user perception about a media object in the form of view +/// count, ratings and tags. +public class MediaCommunity { + + /// This element specifies the rating-related information about a media object. + /// Valid attributes are average, count, min and max. + public var mediaStarRating: MediaStarRating? + + /// This element specifies various statistics about a media object like the + /// view count and the favorite count. Valid attributes are views and favorites. + public var mediaStatistics: MediaStatistics? + + /// This element contains user-generated tags separated by commas in the + /// decreasing order of each tag's weight. Each tag can be assigned an integer + /// weight in tag_name:weight format. It's up to the provider to choose the way + /// weight is determined for a tag; for example, number of occurences can be + /// one way to decide weight of a particular tag. Default weight is 1. + public var mediaTags: [MediaTag]? + +} + +// MARK: - Equatable + +extension MediaCommunity: Equatable { + + public static func ==(lhs: MediaCommunity, rhs: MediaCommunity) -> Bool { + return + lhs.mediaStarRating == rhs.mediaStarRating && + lhs.mediaStatistics == rhs.mediaStatistics && + lhs.mediaTags == rhs.mediaTags + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaContent.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaContent.swift new file mode 100644 index 0000000..57da7aa --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaContent.swift @@ -0,0 +1,176 @@ +// +// MediaContent.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// is a sub-element of either or . +/// Media objects that are not the same content should not be included +/// in the same element. The sequence of these items implies +/// the order of presentation. While many of the attributes appear to be +/// audio/video specific, this element can be used to publish any type of +/// media. It contains 14 attributes, most of which are optional. +public class MediaContent { + + /// The element's attributes. + public class Attributes { + + /// Should specify the direct URL to the media object. If not included, + /// a element must be specified. + public var url: String? + + /// The number of bytes of the media object. It is an optional + /// attribute. + public var fileSize: Int? + + /// The standard MIME type of the object. It is an optional attribute. + public var type: String? + + /// Tpe of object (image | audio | video | document | executable). + /// While this attribute can at times seem redundant if type is supplied, + /// it is included because it simplifies decision making on the reader + /// side, as well as flushes out any ambiguities between MIME type and + /// object type. It is an optional attribute. + public var medium: String? + + /// Determines if this is the default object that should be used for + /// the . There should only be one default object per + /// . It is an optional attribute. + public var isDefault: Bool? + + /// Determines if the object is a sample or the full version of the + /// object, or even if it is a continuous stream (sample | full | nonstop). + /// Default value is "full". It is an optional attribute. + public var expression: String? + + /// The kilobits per second rate of media. It is an optional attribute. + public var bitrate: Int? + + /// The number of frames per second for the media object. It is an + /// optional attribute. + public var framerate: Double? + + /// The number of samples per second taken to create the media object. + /// It is expressed in thousands of samples per second (kHz). + /// It is an optional attribute. + public var samplingrate: Double? + + /// The number of audio channels in the media object. It is an + /// optional attribute. + public var channels: Int? + + /// The number of seconds the media object plays. It is an + /// optional attribute. + public var duration: Int? + + /// The height of the media object. It is an optional attribute. + public var height: Int? + + /// The width of the media object. It is an optional attribute. + public var width: Int? + + /// The primary language encapsulated in the media object. + /// Language codes possible are detailed in RFC 3066. This attribute + /// is used similar to the xml:lang attribute detailed in the + /// XML 1.0 Specification (Third Edition). It is an optional + /// attribute. + public var lang: String? + + } + + /// The element's attributes + public var attributes: Attributes? + +} + +// MARK: - Initializers + +extension MediaContent { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaContent.Attributes(attributes: attributeDict) + } + +} + +extension MediaContent.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.url = attributeDict["url"] + self.fileSize = Int(attributeDict["fileSize"] ?? "") + self.type = attributeDict["type"] + self.medium = attributeDict["medium"] + self.isDefault = attributeDict["isDefault"]?.toBool() + self.expression = attributeDict["expression"] + self.bitrate = Int(attributeDict["bitrate"] ?? "") + self.framerate = Double(attributeDict["framerate"] ?? "") + self.samplingrate = Double(attributeDict["samplingrate"] ?? "") + self.channels = Int(attributeDict["channels"] ?? "") + self.duration = Int(attributeDict["duration"] ?? "") + self.height = Int(attributeDict["height"] ?? "") + self.width = Int(attributeDict["width"] ?? "") + self.lang = attributeDict["lang"] + + } + +} + +// MARK: - Equatable + +extension MediaContent: Equatable { + + public static func ==(lhs: MediaContent, rhs: MediaContent) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension MediaContent.Attributes: Equatable { + + public static func ==(lhs: MediaContent.Attributes, rhs: MediaContent.Attributes) -> Bool { + return + lhs.bitrate == rhs.bitrate && + lhs.channels == rhs.channels && + lhs.duration == rhs.duration && + lhs.expression == rhs.expression && + lhs.isDefault == rhs.isDefault && + lhs.fileSize == rhs.fileSize && + lhs.framerate == rhs.framerate && + lhs.height == rhs.height && + lhs.lang == rhs.lang && + lhs.medium == rhs.medium && + lhs.samplingrate == rhs.samplingrate && + lhs.type == rhs.type && + lhs.url == rhs.url && + lhs.width == rhs.width + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCopyright.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCopyright.swift new file mode 100644 index 0000000..ed3a73f --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCopyright.swift @@ -0,0 +1,95 @@ +// +// MediaCopyright.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Copyright information for the media object. It has one optional attribute. +public class MediaCopyright { + + /// The element's attributes. + public class Attributes { + + /// The URL for a terms of use page or additional copyright information. + /// If the media is operating under a Creative Commons license, the + /// Creative Commons module should be used instead. It is an optional + /// attribute. + public var url: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension MediaCopyright { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaCopyright.Attributes(attributes: attributeDict) + } + +} + + +extension MediaCopyright.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.url = attributeDict["url"] + + } + +} + +// MARK: - Equatable + +extension MediaCopyright: Equatable { + + public static func ==(lhs: MediaCopyright, rhs: MediaCopyright) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaCopyright.Attributes: Equatable { + + public static func ==(lhs: MediaCopyright.Attributes, rhs: MediaCopyright.Attributes) -> Bool { + return lhs.url == rhs.url + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCredit.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCredit.swift new file mode 100644 index 0000000..1e69625 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCredit.swift @@ -0,0 +1,107 @@ +// +// MediaCredit.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Notable entity and the contribution to the creation of the media object. +/// Current entities can include people, companies, locations, etc. Specific +/// entities can have multiple roles, and several entities can have the same +/// role. These should appear as distinct elements. It has two +/// optional attributes. +public class MediaCredit { + + /// The element's attributes. + public class Attributes { + + /// Specifies the role the entity played. Must be lowercase. It is an + /// optional attribute. + public var role: String? + + /// The URI that identifies the role scheme. It is an optional attribute + /// and possible values for this attribute are ( urn:ebu | urn:yvs ) . The + /// default scheme is "urn:ebu". The list of roles supported under urn:ebu + /// scheme can be found at European Broadcasting Union Role Codes. The + /// roles supported under urn:yvs scheme are ( uploader | owner ). + public var scheme: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension MediaCredit { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaCredit.Attributes(attributes: attributeDict) + } + +} + +extension MediaCredit.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.role = attributeDict["role"] + self.scheme = attributeDict["scheme"] + + } + +} + +// MARK: - Equatable + +extension MediaCredit: Equatable { + + public static func ==(lhs: MediaCredit, rhs: MediaCredit) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaCredit.Attributes: Equatable { + + public static func ==(lhs: MediaCredit.Attributes, rhs: MediaCredit.Attributes) -> Bool { + return + lhs.role == rhs.role && + lhs.scheme == rhs.scheme + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaDescription.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaDescription.swift new file mode 100644 index 0000000..af6462a --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaDescription.swift @@ -0,0 +1,94 @@ +// +// MediaDescription.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Short description describing the media object typically a sentence in +/// length. It has one optional attribute. +public class MediaDescription { + + /// The element's attributes. + public class Attributes { + + /// Specifies the type of text embedded. Possible values are either "plain" or "html". + /// Default value is "plain". All HTML must be entity-encoded. It is an optional attribute. + public var type: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension MediaDescription { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaDescription.Attributes(attributes: attributeDict) + } + +} + + +extension MediaDescription.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + + } + +} + +// MARK: - Equatable + +extension MediaDescription: Equatable { + + public static func ==(lhs: MediaDescription, rhs: MediaDescription) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaDescription.Attributes: Equatable { + + public static func ==(lhs: MediaDescription.Attributes, rhs: MediaDescription.Attributes) -> Bool { + return lhs.type == rhs.type + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaEmbed.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaEmbed.swift new file mode 100644 index 0000000..1fac259 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaEmbed.swift @@ -0,0 +1,104 @@ +// +// MediaEmbed.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Sometimes player-specific embed code is needed for a player to play any +/// video. allows inclusion of such information in the form of +/// key-value pairs. +public class MediaEmbed { + + /// The element's attributes. + public class Attributes { + + /// The location of the embeded media. + public var url: String? + + /// The width size for the embeded Media. + public var width: Int? + + /// The height size for the embeded Media. + public var height: Int? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// Key-Value pairs with aditional parameters for the embeded Media. + public var mediaParams: [MediaParam]? + +} + +// MARK: - Initializers + +extension MediaEmbed { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaEmbed.Attributes(attributes: attributeDict) + } + +} + +extension MediaEmbed.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.url = attributeDict["url"] + self.width = Int(attributeDict["width"] ?? "") + self.height = Int(attributeDict["height"] ?? "") + + } + +} + +// MARK: - Equatable + +extension MediaEmbed: Equatable { + + public static func ==(lhs: MediaEmbed, rhs: MediaEmbed) -> Bool { + return + lhs.mediaParams == rhs.mediaParams && + lhs.attributes == rhs.attributes + } + +} + +extension MediaEmbed.Attributes: Equatable { + + public static func ==(lhs: MediaEmbed.Attributes, rhs: MediaEmbed.Attributes) -> Bool { + return + lhs.url == rhs.url && + lhs.width == rhs.width && + lhs.height == rhs.height + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaGroup.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaGroup.swift new file mode 100644 index 0000000..99ee530 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaGroup.swift @@ -0,0 +1,72 @@ +// +// MediaGroup.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The element is a sub-element of . It allows grouping +/// of elements that are effectively the same content, +/// yet different representations. For instance: the same song recorded +/// in both the WAV and MP3 format. It's an optional element that must +/// only be used for this purpose. +public class MediaGroup { + + /// is a sub-element of either or . + /// Media objects that are not the same content should not be included + /// in the same element. The sequence of these items implies + /// the order of presentation. While many of the attributes appear to be + /// audio/video specific, this element can be used to publish any type of + /// media. It contains 14 attributes, most of which are optional. + public var mediaContents: [MediaContent]? + + /// Notable entity and the contribution to the creation of the media object. + /// Current entities can include people, companies, locations, etc. Specific + /// entities can have multiple roles, and several entities can have the same + /// role. These should appear as distinct elements. It has two + /// optional attributes. + public var mediaCredits: [MediaCredit]? + + /// Allows a taxonomy to be set that gives an indication of the type of media + /// content, and its particular contents. It has two optional attributes. + public var mediaCategory: MediaCategory? + + /// This allows the permissible audience to be declared. If this element is not + /// included, it assumes that no restrictions are necessary. It has one + /// optional attribute. + public var mediaRating: MediaRating? + +} + +// MARK: - Equatable + +extension MediaGroup: Equatable { + + public static func ==(lhs: MediaGroup, rhs: MediaGroup) -> Bool { + return + lhs.mediaContents == rhs.mediaContents && + lhs.mediaCredits == rhs.mediaCredits && + lhs.mediaCategory == rhs.mediaCategory && + lhs.mediaRating == rhs.mediaRating + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaHash.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaHash.swift new file mode 100644 index 0000000..397dfcf --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaHash.swift @@ -0,0 +1,95 @@ +// +// MediaHash.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// This is the hash of the binary media file. It can appear multiple times as +/// long as each instance is a different algo. It has one optional attribute. +public class MediaHash { + + /// The element's attributes. + public class Attributes { + + /// This is the hash of the binary media file. It can appear multiple times as long as + /// each instance is a different algo. It has one optional attribute. + public var algo: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension MediaHash { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaHash.Attributes(attributes: attributeDict) + } + +} + + +extension MediaHash.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.algo = attributeDict["algo"] + + } + +} + +// MARK: - Equatable + +extension MediaHash: Equatable { + + public static func ==(lhs: MediaHash, rhs: MediaHash) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaHash.Attributes: Equatable { + + public static func ==(lhs: MediaHash.Attributes, rhs: MediaHash.Attributes) -> Bool { + return lhs.algo == rhs.algo + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaLicence.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaLicence.swift new file mode 100644 index 0000000..01ee611 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaLicence.swift @@ -0,0 +1,99 @@ +// +// MediaLicence.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional link to specify the machine-readable license associated with the +/// content. +public class MediaLicence { + + /// The element's attributes. + public class Attributes { + + /// The licence type. + public var type: String? + + /// The location of the licence. + public var href: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension MediaLicence { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaLicence.Attributes(attributes: attributeDict) + } + +} + +extension MediaLicence.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + self.href = attributeDict["href"] + + } + +} + +// MARK: - Equatable + +extension MediaLicence: Equatable { + + public static func ==(lhs: MediaLicence, rhs: MediaLicence) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaLicence.Attributes: Equatable { + + public static func ==(lhs: MediaLicence.Attributes, rhs: MediaLicence.Attributes) -> Bool { + return + lhs.type == rhs.type && + lhs.href == rhs.href + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaLocation.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaLocation.swift new file mode 100644 index 0000000..df6eed3 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaLocation.swift @@ -0,0 +1,126 @@ +// +// MediaLocation.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional element to specify geographical information about various +/// locations captured in the content of a media object. The format conforms +/// to geoRSS. +public class MediaLocation { + + /// The element's attributes. + public class Attributes { + + /// Description of the place whose location is being specified. + public var description: String? + + /// Time at which the reference to a particular location starts in the + /// media object. + public var start: TimeInterval? + + /// Time at which the reference to a particular location ends in the media + /// object. + public var end: TimeInterval? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The geoRSS's location latitude. + public var latitude: Double? + + /// The geoRSS's location longitude. + public var longitude: Double? + +} + +// MARK: - Initializers + +extension MediaLocation { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaLocation.Attributes(attributes: attributeDict) + } + +} + +extension MediaLocation.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.description = attributeDict["description"] + self.start = attributeDict["start"]?.toDuration() + self.end = attributeDict["end"]?.toDuration() + + } + +} + +// MARK: - Equatable + +extension MediaLocation: Equatable { + + public static func ==(lhs: MediaLocation, rhs: MediaLocation) -> Bool { + return + lhs.latitude == rhs.latitude && + lhs.longitude == rhs.longitude && + lhs.attributes == rhs.attributes + } + +} + +extension MediaLocation.Attributes: Equatable { + + public static func ==(lhs: MediaLocation.Attributes, rhs: MediaLocation.Attributes) -> Bool { + return + lhs.description == rhs.description && + lhs.start == rhs.start && + lhs.end == rhs.end + } + +} + +// MARK: - Helpers + +extension MediaLocation { + + func mapFrom(latLng: String) { + + let components = latLng.components(separatedBy: " ") + if components.count == 2 { + self.latitude = Double(components.first ?? "") + self.longitude = Double(components.last ?? "") + } + + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaNamespace.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaNamespace.swift new file mode 100644 index 0000000..af08d24 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaNamespace.swift @@ -0,0 +1,215 @@ +// +// MediaNamespace.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Media RSS is a new RSS module that supplements the +/// capabilities of RSS 2.0. RSS enclosures are already being used to +/// syndicate audio files and images. Media RSS extends enclosures to +/// handle other media types, such as short films or TV, as well as +/// provide additional metadata with the media. Media RSS enables +/// content publishers and bloggers to syndicate multimedia content +/// such as TV and video clips, movies, images and audio. +public class MediaNamespace { + + /// The element is a sub-element of . It allows grouping + /// of elements that are effectively the same content, + /// yet different representations. For instance: the same song recorded + /// in both the WAV and MP3 format. It's an optional element that must + /// only be used for this purpose. + public var mediaGroup: MediaGroup? + + /// is a sub-element of either or . + /// Media objects that are not the same content should not be included + /// in the same element. The sequence of these items implies + /// the order of presentation. While many of the attributes appear to be + /// audio/video specific, this element can be used to publish any type of + /// media. It contains 14 attributes, most of which are optional. + public var mediaContents: [MediaContent]? + + /// This allows the permissible audience to be declared. If this element is not + /// included, it assumes that no restrictions are necessary. It has one + /// optional attribute. + public var mediaRating: MediaRating? + + /// The title of the particular media object. It has one optional attribute. + public var mediaTitle: MediaTitle? + + /// Short description describing the media object typically a sentence in + /// length. It has one optional attribute. + public var mediaDescription: MediaDescription? + + /// Highly relevant keywords describing the media object with typically a + /// maximum of 10 words. The keywords and phrases should be comma-delimited. + public var mediaKeywords: [String]? + + /// Allows particular images to be used as representative images for the + /// media object. If multiple thumbnails are included, and time coding is not + /// at play, it is assumed that the images are in order of importance. It has + /// one required attribute and three optional attributes. + public var mediaThumbnails: [MediaThumbnail]? + + /// Allows a taxonomy to be set that gives an indication of the type of media + /// content, and its particular contents. It has two optional attributes. + public var mediaCategory: MediaCategory? + + /// This is the hash of the binary media file. It can appear multiple times as + /// long as each instance is a different algo. It has one optional attribute. + public var mediaHash: MediaHash? + + /// Allows the media object to be accessed through a web browser media player + /// console. This element is required only if a direct media url attribute is + /// not specified in the element. It has one required attribute + /// and two optional attributes. + public var mediaPlayer: MediaPlayer? + + /// Notable entity and the contribution to the creation of the media object. + /// Current entities can include people, companies, locations, etc. Specific + /// entities can have multiple roles, and several entities can have the same + /// role. These should appear as distinct elements. It has two + /// optional attributes. + public var mediaCredits: [MediaCredit]? + + /// Copyright information for the media object. It has one optional attribute. + public var mediaCopyright: MediaCopyright? + + /// Allows the inclusion of a text transcript, closed captioning or lyrics of + /// the media content. Many of these elements are permitted to provide a time + /// series of text. In such cases, it is encouraged, but not required, that the + /// elements be grouped by language and appear in time sequence order based on + /// the start time. Elements can have overlapping start and end times. It has + /// four optional attributes. + public var mediaText: MediaText? + + /// Allows restrictions to be placed on the aggregator rendering the media in + /// the feed. Currently, restrictions are based on distributor (URI), country + /// codes and sharing of a media object. This element is purely informational + /// and no obligation can be assumed or implied. Only one + /// element of the same type can be applied to a media object -- all others + /// will be ignored. Entities in this element should be space-separated. + /// To allow the producer to explicitly declare his/her intentions, two + /// literals are reserved: "all", "none". These literals can only be used once. + /// This element has one required attribute and one optional attribute (with + /// strict requirements for its exclusion). + public var mediaRestriction: MediaRestriction? + + /// This element stands for the community related content. This allows + /// inclusion of the user perception about a media object in the form of view + /// count, ratings and tags. + public var mediaCommunity: MediaCommunity? + + /// Allows inclusion of all the comments a media object has received. + public var mediaComments: [String]? + + /// Sometimes player-specific embed code is needed for a player to play any + /// video. allows inclusion of such information in the form of + /// key-value pairs. + public var mediaEmbed: MediaEmbed? + + /// Allows inclusion of a list of all media responses a media object has + /// received. + public var mediaResponses: [String]? + + /// Allows inclusion of all the URLs pointing to a media object. + public var mediaBackLinks: [String]? + + /// Optional tag to specify the status of a media object -- whether it's still + /// active or it has been blocked/deleted. + public var mediaStatus: MediaStatus? + + /// Optional tag to include pricing information about a media object. If this + /// tag is not present, the media object is supposed to be free. One media + /// object can have multiple instances of this tag for including different + /// pricing structures. The presence of this tag would mean that media object + /// is not free. + public var mediaPrices: [MediaPrice]? + + /// Optional link to specify the machine-readable license associated with the + /// content. + public var mediaLicense: MediaLicence? + + /// Optional link to specify the machine-readable license associated with the + /// content. + public var mediaSubTitle: MediaSubTitle? + + /// Optional element for P2P link. + public var mediaPeerLink: MediaPeerLink? + + /// Optional element to specify geographical information about various + /// locations captured in the content of a media object. The format conforms + /// to geoRSS. + public var mediaLocation: MediaLocation? + + /// Optional element to specify the rights information of a media object. + public var mediaRights: MediaRights? + + /// Optional element to specify various scenes within a media object. It can + /// have multiple child elements, where each + /// element contains information about a particular scene. has + /// the optional sub-elements , , + /// and , which contains title, description, + /// start and end time of a particular scene in the media, respectively. + public var mediaScenes: [MediaScene]? + + +} + +// MARK: - Equatable + +extension MediaNamespace: Equatable { + + public static func ==(lhs: MediaNamespace, rhs: MediaNamespace) -> Bool { + return + lhs.mediaGroup == rhs.mediaGroup && + lhs.mediaContents == rhs.mediaContents && + lhs.mediaRating == rhs.mediaRating && + lhs.mediaTitle == rhs.mediaTitle && + lhs.mediaDescription == rhs.mediaDescription && + lhs.mediaKeywords == rhs.mediaKeywords && + lhs.mediaThumbnails == rhs.mediaThumbnails && + lhs.mediaCategory == rhs.mediaCategory && + lhs.mediaHash == rhs.mediaHash && + lhs.mediaPlayer == rhs.mediaPlayer && + lhs.mediaCredits == rhs.mediaCredits && + lhs.mediaCopyright == rhs.mediaCopyright && + lhs.mediaText == rhs.mediaText && + lhs.mediaRestriction == rhs.mediaRestriction && + lhs.mediaCommunity == rhs.mediaCommunity && + lhs.mediaComments == rhs.mediaComments && + lhs.mediaEmbed == rhs.mediaEmbed && + lhs.mediaResponses == rhs.mediaResponses && + lhs.mediaBackLinks == rhs.mediaBackLinks && + lhs.mediaStatus == rhs.mediaStatus && + lhs.mediaPrices == rhs.mediaPrices && + lhs.mediaLicense == rhs.mediaLicense && + lhs.mediaSubTitle == rhs.mediaSubTitle && + lhs.mediaPeerLink == rhs.mediaPeerLink && + lhs.mediaLocation == rhs.mediaLocation && + lhs.mediaRights == rhs.mediaRights && + lhs.mediaScenes == rhs.mediaScenes + } + +} + + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaParam.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaParam.swift new file mode 100644 index 0000000..ffa5fd1 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaParam.swift @@ -0,0 +1,92 @@ +// +// MediaParam.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Key-Value pairs with aditional parameters for the embeded Media. +public class MediaParam { + + /// The element's attributes. + public class Attributes { + + /// The parameter's key name. + public var name: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension MediaParam { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaParam.Attributes(attributes: attributeDict) + } + +} + +extension MediaParam.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.name = attributeDict["name"] + + } + +} + +// MARK: - Equatable + +extension MediaParam: Equatable { + + public static func ==(lhs: MediaParam, rhs: MediaParam) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaParam.Attributes: Equatable { + + public static func ==(lhs: MediaParam.Attributes, rhs: MediaParam.Attributes) -> Bool { + return lhs.name == rhs.name + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPeerLink.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPeerLink.swift new file mode 100644 index 0000000..b243f8b --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPeerLink.swift @@ -0,0 +1,98 @@ +// +// MediaPeerLink.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional element for P2P link. +public class MediaPeerLink { + + /// The element's attributes. + public class Attributes { + + /// The peer link's type. + public var type: String? + + /// The location of the peer link provider. + public var href: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension MediaPeerLink { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaPeerLink.Attributes(attributes: attributeDict) + } + +} + +extension MediaPeerLink.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + self.href = attributeDict["href"] + + } + +} + +// MARK: - Equatable + +extension MediaPeerLink: Equatable { + + public static func ==(lhs: MediaPeerLink, rhs: MediaPeerLink) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaPeerLink.Attributes: Equatable { + + public static func ==(lhs: MediaPeerLink.Attributes, rhs: MediaPeerLink.Attributes) -> Bool { + return + lhs.type == rhs.type && + lhs.href == rhs.href + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPlayer.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPlayer.swift new file mode 100644 index 0000000..9421a9c --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPlayer.swift @@ -0,0 +1,108 @@ +// +// MediaPlayer.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Allows the media object to be accessed through a web browser media player +/// console. This element is required only if a direct media url attribute is +/// not specified in the element. It has one required attribute +/// and two optional attributes. +public class MediaPlayer { + + /// The element's attributes. + public class Attributes { + + /// The URL of the player console that plays the media. It is a required attribute. + public var url: String? + + /// The width of the browser window that the URL should be opened in. It is + /// an optional attribute. + public var width: Int? + + /// The height of the browser window that the URL should be opened in. It is an + /// optional attribute. + public var height: Int? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension MediaPlayer { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaPlayer.Attributes(attributes: attributeDict) + } + +} + + +extension MediaPlayer.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.url = attributeDict["algo"] + self.height = Int(attributeDict["height"] ?? "") + self.width = Int(attributeDict["width"] ?? "") + + } + +} + +// MARK: - Equatable + +extension MediaPlayer: Equatable { + + public static func ==(lhs: MediaPlayer, rhs: MediaPlayer) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaPlayer.Attributes: Equatable { + + public static func ==(lhs: MediaPlayer.Attributes, rhs: MediaPlayer.Attributes) -> Bool { + return + lhs.width == rhs.width && + lhs.height == rhs.height && + lhs.url == rhs.url + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPrice.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPrice.swift new file mode 100644 index 0000000..14bc8a2 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPrice.swift @@ -0,0 +1,114 @@ +// +// MediaPrice.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional tag to include pricing information about a media object. If this +/// tag is not present, the media object is supposed to be free. One media +/// object can have multiple instances of this tag for including different +/// pricing structures. The presence of this tag would mean that media object +/// is not free. +public class MediaPrice { + + /// The element's attributes. + public class Attributes { + + /// Valid values are "rent", "purchase", "package" or "subscription". If + /// nothing is specified, then the media is free. + public var type: String? + + /// The price of the media object. This is an optional attribute. + public var price: Double? + + /// If the type is "package" or "subscription", then info is a URL pointing + /// to package or subscription information. This is an optional attribute. + public var info: String? + + /// Use [ISO 4217] for currency codes. This is an optional attribute. + public var currency: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension MediaPrice { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaPrice.Attributes(attributes: attributeDict) + } + +} + +extension MediaPrice.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + self.price = Double(attributeDict["price"] ?? "") + self.info = attributeDict["info"] + self.currency = attributeDict["currency"] + + } + +} + +// MARK: - Equatable + +extension MediaPrice: Equatable { + + public static func ==(lhs: MediaPrice, rhs: MediaPrice) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaPrice.Attributes: Equatable { + + public static func ==(lhs: MediaPrice.Attributes, rhs: MediaPrice.Attributes) -> Bool { + return + lhs.type == rhs.type && + lhs.price == rhs.price && + lhs.info == rhs.info && + lhs.currency == rhs.currency + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRating.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRating.swift new file mode 100644 index 0000000..afd8b2b --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRating.swift @@ -0,0 +1,96 @@ +// +// MediaRating.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// This allows the permissible audience to be declared. If this element is not +/// included, it assumes that no restrictions are necessary. It has one optional +/// attribute. +public class MediaRating { + + /// The element's attributes. + public class Attributes { + + /// The URI that identifies the rating scheme. It is an optional attribute. + /// If this attribute is not included, the default scheme is urn:simple (adult | nonadult). + public var scheme: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension MediaRating { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaRating.Attributes(attributes: attributeDict) + } + +} + + +extension MediaRating.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.scheme = attributeDict["scheme"] + + } + +} + +// MARK: - Equatable + +extension MediaRating: Equatable { + + public static func ==(lhs: MediaRating, rhs: MediaRating) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaRating.Attributes: Equatable { + + public static func ==(lhs: MediaRating.Attributes, rhs: MediaRating.Attributes) -> Bool { + return lhs.scheme == rhs.scheme + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRestriction.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRestriction.swift new file mode 100644 index 0000000..9f319e5 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRestriction.swift @@ -0,0 +1,116 @@ +// +// MediaRestriction.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Allows restrictions to be placed on the aggregator rendering the media in +/// the feed. Currently, restrictions are based on distributor (URI), country +/// codes and sharing of a media object. This element is purely informational +/// and no obligation can be assumed or implied. Only one +/// element of the same type can be applied to a media object -- all others +/// will be ignored. Entities in this element should be space-separated. +/// To allow the producer to explicitly declare his/her intentions, two +/// literals are reserved: "all", "none". These literals can only be used once. +/// This element has one required attribute and one optional attribute (with +/// strict requirements for its exclusion). +public class MediaRestriction { + + /// The element's attributes. + public class Attributes { + + /// Indicates the type of relationship that the restriction represents + /// (allow | deny). In the example above, the media object should only be + /// syndicated in Australia and the United States. It is a required + /// attribute. + /// + /// Note: If the "allow" element is empty and the type of relationship is + /// "allow", it is assumed that the empty list means "allow nobody" and + /// the media should not be syndicated. + public var relationship: String? + + /// Specifies the type of restriction (country | uri | sharing ) that the + /// media can be syndicated. It is an optional attribute; however can only + /// be excluded when using one of the literal values "all" or "none". + public var type: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension MediaRestriction { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaRestriction.Attributes(attributes: attributeDict) + } + +} + + +extension MediaRestriction.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.relationship = attributeDict["relationship"] + self.type = attributeDict["type"] + + } + +} + +// MARK: - Equatable + +extension MediaRestriction: Equatable { + + public static func ==(lhs: MediaRestriction, rhs: MediaRestriction) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaRestriction.Attributes: Equatable { + + public static func ==(lhs: MediaRestriction.Attributes, rhs: MediaRestriction.Attributes) -> Bool { + return + lhs.relationship == rhs.relationship && + lhs.type == rhs.type + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRights.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRights.swift new file mode 100644 index 0000000..aaf24fe --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRights.swift @@ -0,0 +1,89 @@ +// +// MediaRights.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional element to specify the rights information of a media object. +public class MediaRights { + + /// The element's attributes. + public class Attributes { + + /// Is the status of the media object saying whether a media object has + /// been created by the publisher or they have rights to circulate it. + /// Supported values are "userCreated" and "official". + public var status: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + +} + +// MARK: - Initializers + +extension MediaRights { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaRights.Attributes(attributes: attributeDict) + } + +} + +extension MediaRights.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.status = attributeDict["status"] + + } + +} + +// MARK: - Equatable + +extension MediaRights: Equatable { + + public static func ==(lhs: MediaRights, rhs: MediaRights) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension MediaRights.Attributes: Equatable { + + public static func ==(lhs: MediaRights.Attributes, rhs: MediaRights.Attributes) -> Bool { + return lhs.status == rhs.status + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaScene.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaScene.swift new file mode 100644 index 0000000..2025954 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaScene.swift @@ -0,0 +1,61 @@ +// +// MediaScene.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional element to specify various scenes within a media object. It can +/// have multiple child elements, where each +/// element contains information about a particular scene. has +/// the optional sub-elements , , +/// and , which contains title, description, +/// start and end time of a particular scene in the media, respectively. +public class MediaScene { + + /// The scene's title. + public var sceneTitle: String? + + /// The scene's description. + public var sceneDescription: String? + + /// The scene's start time. + public var sceneStartTime: TimeInterval? + + /// The scene's end time. + public var sceneEndTime: TimeInterval? + +} + +// MARK: - Equatable + +extension MediaScene: Equatable { + + public static func ==(lhs: MediaScene, rhs: MediaScene) -> Bool { + return + lhs.sceneTitle == rhs.sceneTitle && + lhs.sceneDescription == rhs.sceneDescription && + lhs.sceneStartTime == rhs.sceneStartTime && + lhs.sceneEndTime == rhs.sceneEndTime + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStarRating.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStarRating.swift new file mode 100644 index 0000000..b4c46ca --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStarRating.swift @@ -0,0 +1,104 @@ +// +// MediaStarRating.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// This element specifies the rating-related information about a media object. +/// Valid attributes are average, count, min and max. +public class MediaStarRating { + + /// The element's attributes. + public class Attributes { + + /// The star rating's average. + public var average: Double? + + /// The star rating's total count. + public var count: Int? + + /// The star rating's minimum value. + public var min: Int? + + /// The star rating's maximum value. + public var max: Int? + + } + + /// The element's attributes. + public var attributes: Attributes? + +} + +// MARK: - Initializers + +extension MediaStarRating { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaStarRating.Attributes(attributes: attributeDict) + } + +} + +extension MediaStarRating.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.average = Double(attributeDict["average"] ?? "") + self.count = Int(attributeDict["count"] ?? "") + self.min = Int(attributeDict["min"] ?? "") + self.max = Int(attributeDict["max"] ?? "") + + } + +} + +// MARK: - Equatable + +extension MediaStarRating: Equatable { + + public static func ==(lhs: MediaStarRating, rhs: MediaStarRating) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension MediaStarRating.Attributes: Equatable { + + public static func ==(lhs: MediaStarRating.Attributes, rhs: MediaStarRating.Attributes) -> Bool { + return + lhs.average == rhs.average && + lhs.count == rhs.count && + lhs.min == rhs.min && + lhs.max == rhs.max + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStatistics.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStatistics.swift new file mode 100644 index 0000000..283c8f1 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStatistics.swift @@ -0,0 +1,94 @@ +// +// MediaStatistics.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// This element specifies various statistics about a media object like the +/// view count and the favorite count. Valid attributes are views and favorites. +public class MediaStatistics { + + /// The element's attributes. + public class Attributes { + + /// The number of views. + public var views: Int? + + /// The number fo favorites. + public var favorites: Int? + + } + + /// The element's attributes. + public var attributes: Attributes? + +} + +// MARK: - Initializers + +extension MediaStatistics { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaStatistics.Attributes(attributes: attributeDict) + } + +} + +extension MediaStatistics.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.views = Int(attributeDict["views"] ?? "") + self.favorites = Int(attributeDict["favorites"] ?? "") + + } + +} + +// MARK: - Equatable + +extension MediaStatistics: Equatable { + + public static func ==(lhs: MediaStatistics, rhs: MediaStatistics) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension MediaStatistics.Attributes: Equatable { + + public static func ==(lhs: MediaStatistics.Attributes, rhs: MediaStatistics.Attributes) -> Bool { + return + lhs.views == rhs.views && + lhs.favorites == rhs.favorites + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStatus.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStatus.swift new file mode 100644 index 0000000..74c9d64 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStatus.swift @@ -0,0 +1,98 @@ +// +// MediaStatus.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional tag to specify the status of a media object -- whether it's still +/// active or it has been blocked/deleted. +public class MediaStatus { + + /// The element's attributes. + public class Attributes { + + /// State can have values "active", "blocked" or "deleted". "active" means + /// a media object is active in the system, "blocked" means a media object + /// is blocked by the publisher, "deleted" means a media object has been + /// deleted by the publisher. + public var state: String? + + /// A reason explaining why a media object has been blocked/deleted. It can + /// be plain text or a URL. + public var reason: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + +} + +// MARK: - Initializers + +extension MediaStatus { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaStatus.Attributes(attributes: attributeDict) + } + +} + +extension MediaStatus.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.state = attributeDict["state"] + self.reason = attributeDict["reason"] + + } + +} + +// MARK: - Equatable + +extension MediaStatus: Equatable { + + public static func ==(lhs: MediaStatus, rhs: MediaStatus) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension MediaStatus.Attributes: Equatable { + + public static func ==(lhs: MediaStatus.Attributes, rhs: MediaStatus.Attributes) -> Bool { + return + lhs.state == rhs.state && + lhs.reason == rhs.reason + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaSubTitle.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaSubTitle.swift new file mode 100644 index 0000000..c4b389d --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaSubTitle.swift @@ -0,0 +1,98 @@ +// +// MediaSubTitle.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional link to specify the machine-readable license associated with the +/// content. +public class MediaSubTitle { + + /// The element's attributes. + public class Attributes { + + /// The type of the subtitle. + public var type: String? + + /// The subtitle language based on the RFC 3066. + public var lang: String? + + /// The location of the subtitle. + public var href: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + +} + +// MARK: - Initializers + +extension MediaSubTitle { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaSubTitle.Attributes(attributes: attributeDict) + } + +} + +extension MediaSubTitle.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + self.lang = attributeDict["lang"] + self.href = attributeDict["href"] + + } + +} + +// MARK: - Equatable + +extension MediaSubTitle: Equatable { + + public static func ==(lhs: MediaSubTitle, rhs: MediaSubTitle) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension MediaSubTitle.Attributes: Equatable { + + public static func ==(lhs: MediaSubTitle.Attributes, rhs: MediaSubTitle.Attributes) -> Bool { + return + lhs.type == rhs.type && + lhs.lang == rhs.lang && + lhs.href == rhs.href + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaTag.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaTag.swift new file mode 100644 index 0000000..8832105 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaTag.swift @@ -0,0 +1,88 @@ +// +// MediaTag.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// This element contains user-generated tags separated by commas in the decreasing +/// order of each tag's weight. Each tag can be assigned an integer weight in +/// tag_name:weight format. It's up to the provider to choose the way weight is +/// determined for a tag; for example, number of occurences can be one way to +/// decide weight of a particular tag. Default weight is 1. +public class MediaTag { + + /// The tag name. + public var tag: String? + + /// The tag weight. Default to 1 if not specified. + public var weight: Int? = 1 + +} + +// MARK: - Initializers + +extension MediaTag { + + convenience init(tag: String, weight: Int = 1) { + + self.init() + + self.tag = tag + self.weight = weight + + } + + static func tagsFrom(string: String) -> [MediaTag]? { + + return string.components(separatedBy: ",").flatMap({ (value) -> MediaTag? in + + let mediaTag = MediaTag() + let components = value.components(separatedBy: ":") + + if components.count > 0 { + mediaTag.tag = components.first?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + } + + if components.count > 1 { + mediaTag.weight = Int(components.last?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) ?? "") + } + + return mediaTag + + }) + + } + +} + +// MARK: - Equatable + +extension MediaTag: Equatable { + + public static func ==(lhs: MediaTag, rhs: MediaTag) -> Bool { + return + lhs.tag == rhs.tag && + lhs.weight == rhs.weight + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaText.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaText.swift new file mode 100644 index 0000000..0d9153e --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaText.swift @@ -0,0 +1,124 @@ +// +// MediaText.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Allows the inclusion of a text transcript, closed captioning or lyrics of +/// the media content. Many of these elements are permitted to provide a time +/// series of text. In such cases, it is encouraged, but not required, that the +/// elements be grouped by language and appear in time sequence order based on +/// the start time. Elements can have overlapping start and end times. It has +/// four optional attributes. +public class MediaText { + + /// The element's attributes. + public class Attributes { + + /// Specifies the type of text embedded. Possible values are either "plain" + /// or "html". Default value is "plain". All HTML must be entity-encoded. + /// It is an optional attribute. + public var type: String? + + /// The primary language encapsulated in the media object. Language codes + /// possible are detailed in RFC 3066. This attribute is used similar to + /// the xml:lang attribute detailed in the XML 1.0 Specification (Third + /// Edition). It is an optional attribute. + public var lang: String? + + /// Specifies the start time offset that the text starts being relevant to + /// the media object. An example of this would be for closed captioning. + /// It uses the NTP time code format (see: the time attribute used in + /// ). It is an optional attribute. + public var start: String? + + /// Specifies the end time that the text is relevant. If this attribute is + /// not provided, and a start time is used, it is expected that the end + /// time is either the end of the clip or the start of the next + /// element. + public var end: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension MediaText { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaText.Attributes(attributes: attributeDict) + } + +} + + +extension MediaText.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + self.lang = attributeDict["lang"] + self.start = attributeDict["start"] + self.end = attributeDict["end"] + + } + +} + +// MARK: - Equatable + +extension MediaText: Equatable { + + public static func ==(lhs: MediaText, rhs: MediaText) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaText.Attributes: Equatable { + + public static func ==(lhs: MediaText.Attributes, rhs: MediaText.Attributes) -> Bool { + return + lhs.type == rhs.type && + lhs.lang == rhs.lang && + lhs.start == rhs.start && + lhs.end == rhs.end + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaThumbnail.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaThumbnail.swift new file mode 100644 index 0000000..2db96aa --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaThumbnail.swift @@ -0,0 +1,114 @@ +// +// MediaThumbnail.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Allows particular images to be used as representative images for the +/// media object. If multiple thumbnails are included, and time coding is not +/// at play, it is assumed that the images are in order of importance. It has +/// one required attribute and three optional attributes. +public class MediaThumbnail { + + /// The element's attributes. + public class Attributes { + + /// Specifies the url of the thumbnail. It is a required attribute. + public var url: String? + + /// Specifies the height of the thumbnail. It is an optional attribute. + public var width: String? + + /// Specifies the width of the thumbnail. It is an optional attribute. + public var height: String? + + /// Specifies the time offset in relation to the media object. Typically this + /// is used when creating multiple keyframes within a single video. The format + /// for this attribute should be in the DSM-CC's Normal Play Time (NTP) as used in + /// RTSP [RFC 2326 3.6 Normal Play Time]. It is an optional attribute. + public var time: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension MediaThumbnail { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaThumbnail.Attributes(attributes: attributeDict) + } + +} + + +extension MediaThumbnail.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.url = attributeDict["url"] + self.height = attributeDict["height"] + self.width = attributeDict["width"] + self.time = attributeDict["time"] + + } + +} + +// MARK: - Equatable + +extension MediaThumbnail: Equatable { + + public static func ==(lhs: MediaThumbnail, rhs: MediaThumbnail) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaThumbnail.Attributes: Equatable { + + public static func ==(lhs: MediaThumbnail.Attributes, rhs: MediaThumbnail.Attributes) -> Bool { + return + lhs.url == rhs.url && + lhs.height == rhs.height && + lhs.width == rhs.height && + lhs.time == rhs.time + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaTitle.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaTitle.swift new file mode 100644 index 0000000..6fac865 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaTitle.swift @@ -0,0 +1,94 @@ +// +// MediaTitle.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The title of the particular media object. It has one optional attribute. +public class MediaTitle { + + /// The element's attributes. + public class Attributes { + + /// Specifies the type of text embedded. Possible values are either "plain" or "html". + /// Default value is "plain". All HTML must be entity-encoded. It is an optional attribute. + public var type: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension MediaTitle { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaTitle.Attributes(attributes: attributeDict) + } + +} + + +extension MediaTitle.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + + } + +} + +// MARK: - Equatable + +extension MediaTitle: Equatable { + + public static func ==(lhs: MediaTitle, rhs: MediaTitle) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaTitle.Attributes: Equatable { + + public static func ==(lhs: MediaTitle.Attributes, rhs: MediaTitle.Attributes) -> Bool { + return lhs.type == rhs.type + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Syndication/SyndicationNamespace.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Syndication/SyndicationNamespace.swift new file mode 100644 index 0000000..6c4164a --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Syndication/SyndicationNamespace.swift @@ -0,0 +1,67 @@ +// +// SyndicationNamespace.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Provides syndication hints to aggregators and others picking up this RDF Site +/// Summary (RSS) feed regarding how often it is updated. For example, if you +/// updated your file twice an hour, updatePeriod would be "hourly" and +/// updateFrequency would be "2". The syndication module borrows from Ian Davis's +/// Open Content Syndication (OCS) directory format. It supercedes the RSS 0.91 +/// skipDay and skipHour elements. +/// +/// See http://web.resource.org/rss/1.0/modules/syndication/ +public class SyndicationNamespace { + + /// Describes the period over which the channel format is updated. Acceptable + /// values are: hourly, daily, weekly, monthly, yearly. If omitted, daily is + /// assumed. + public var syUpdatePeriod: SyndicationUpdatePeriod? + + /// Used to describe the frequency of updates in relation to the update period. + /// A positive integer indicates how many times in that period the channel is + /// updated. For example, an updatePeriod of daily, and an updateFrequency of + /// 2 indicates the channel format is updated twice daily. If omitted a value + /// of 1 is assumed. + public var syUpdateFrequency: Int? + + /// Defines a base date to be used in concert with updatePeriod and + /// updateFrequency to calculate the publishing schedule. The date format takes + /// the form: yyyy-mm-ddThh:mm + public var syUpdateBase: Date? + +} + +// MARK: - Equatable + +extension SyndicationNamespace: Equatable { + + public static func ==(lhs: SyndicationNamespace, rhs: SyndicationNamespace) -> Bool { + return + lhs.syUpdatePeriod == rhs.syUpdatePeriod && + lhs.syUpdateFrequency == rhs.syUpdateFrequency && + lhs.syUpdateBase == rhs.syUpdateBase + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Syndication/SyndicationUpdatePeriod.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Syndication/SyndicationUpdatePeriod.swift new file mode 100644 index 0000000..40e8079 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Syndication/SyndicationUpdatePeriod.swift @@ -0,0 +1,69 @@ +// +// SyndicationUpdatePeriod.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Describes the period over which the channel format is updated. Acceptable +/// values are: hourly, daily, weekly, monthly, yearly. If omitted, daily is +/// assumed. +/// +/// - hourly: Every hour, the channel is updated the number of times specified +/// by `syUpdateFrequency` +/// +/// - daily: Every day, the channel is updated the number of times specified +/// by `syUpdateFrequency` +/// +/// - weekly: Every week, the channel is updated the number of times specified +/// by `syUpdateFrequency` +/// +/// - monthly: Every month, the channel is updated the number of times specified +/// by `syUpdateFrequency` +/// +/// - yearly: Every year, the channel is updated the number of times specified +public enum SyndicationUpdatePeriod: String { + case hourly = "hourly" + case daily = "daily" + case weekly = "weekly" + case monthly = "monthly" + case yearly = "yearly" +} + +extension SyndicationUpdatePeriod { + + /// Lowercase the incoming `rawValue` string to try and match the + /// `SyUpdatePeriod`'s `rawValue` + /// + /// - Parameter rawValue: The raw value. + public init?(rawValue: String) { + switch rawValue.lowercased() { + case "hourly": self = .hourly + case "daily": self = .daily + case "weekly": self = .weekly + case "monthly": self = .monthly + case "yearly": self = .yearly + default: return nil + } + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesCategory.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesCategory.swift new file mode 100644 index 0000000..28c236f --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesCategory.swift @@ -0,0 +1,119 @@ +// +// iTunesCategory.swift +// +// Copyright (c) 2017 Ben Murphy +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Users can browse podcast subject categories in the iTunes Store by choosing +/// a category from the Podcasts pop-up menu in the navigation bar. Use the +/// tag to specify the browsing category for your podcast. +/// +/// You can also define a subcategory if one is available within your category. +/// Although you can specify more than one category and subcategory in your +/// feed, the iTunes Store only recognizes the first category and subcategory. +/// For a complete list of categories and subcategories, see Podcasts Connect +/// categories. +/// +/// Note: When specifying categories and subcategories, be sure to properly +/// escape ampersands: +/// +/// Single category: +/// +/// +/// Category with ampersand: +/// +/// +/// Category with subcategory: +/// +/// +/// +/// +/// Multiple categories: +/// +/// +/// +/// +/// +/// +public class ITunesCategory { + + /// The attributes of the element. + public class Attributes { + + /// The primary iTunes Category. + public var text: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The iTunes SubCategory. + public var subcategory: ITunesSubCategory? + +} + +// MARK: - Initializers + +extension ITunesCategory { + + convenience init(attributes attributesDict: [String: String]) { + self.init() + self.attributes = ITunesCategory.Attributes(attributes: attributesDict) + } +} + +extension ITunesCategory.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.text = attributeDict["text"] + + } + +} + +// MARK: - Equatable + +extension ITunesCategory: Equatable { + + public static func ==(lhs: ITunesCategory, rhs: ITunesCategory) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension ITunesCategory.Attributes: Equatable { + + public static func ==(lhs: ITunesCategory.Attributes, rhs: ITunesCategory.Attributes) -> Bool { + return lhs.text == rhs.text + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesImage.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesImage.swift new file mode 100644 index 0000000..c15c7af --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesImage.swift @@ -0,0 +1,110 @@ +// +// ITunesImage.swift +// +// Copyright (c) 2017 Ben Murphy +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Specify your podcast artwork using the attribute in the +/// tag. If you do not specify the tag, the +/// iTunes Store uses the content specified in the RSS feed image tag and Apple +/// does not consider your podcast for feature placement on the iTunes Store or +/// Podcasts. +/// +/// Depending on their device, subscribers see your podcast artwork in varying +/// sizes. Therefore, make sure your design is effective at both its original +/// size and at thumbnail size. Apple recommends including a title, brand, or +/// source name as part of your podcast artwork. For examples of podcast +/// artwork, see the Top Podcasts. To avoid technical issues when you update +/// your podcast artwork, be sure to: +/// +/// Change the artwork file name and URL at the same time +/// Verify the web server hosting your artwork allows HTTP head requests +/// The tag is also supported at the (episode) level. +/// For best results, Apple recommends embedding the same artwork within the +/// metadata for that episode's media file prior to uploading to your host +/// server; using Garageband or another content-creation tool to edit your +/// media file if needed. +/// +/// Note: Artwork must be a minimum size of 1400 x 1400 pixels and a maximum +/// size of 3000 x 3000 pixels, in JPEG or PNG format, 72 dpi, with appropriate +/// file extensions (.jpg, .png), and in the RGB colorspace. These requirements +/// are different from the standard RSS image tag specifications. +public class ITunesImage { + + /// The attributes of the element. + public class Attributes { + + /// The image's url. + public var href: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + +} + +// MARK: - Initializers + +extension ITunesImage { + + convenience init(attributes attributesDict: [String: String]) { + self.init() + self.attributes = ITunesImage.Attributes(attributes: attributesDict) + } +} + +extension ITunesImage.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.href = attributeDict["href"] + + } + +} + +// MARK: - Equatable + +extension ITunesImage: Equatable { + + public static func ==(lhs: ITunesImage, rhs: ITunesImage) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension ITunesImage.Attributes: Equatable { + + public static func ==(lhs: ITunesImage.Attributes, rhs: ITunesImage.Attributes) -> Bool { + return lhs.href == rhs.href + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesNamespace.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesNamespace.swift new file mode 100644 index 0000000..2c154c4 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesNamespace.swift @@ -0,0 +1,235 @@ +// +// iTunesNamespace.swift +// +// Copyright (c) 2017 Ben Murphy +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// iTunes Podcasting Tags are de facto standard for podcast syndication. For more +/// information see https://help.apple.com/itc/podcasts_connect/#/itcb54353390 +public class ITunesNamespace { + + /// The content you specify in the tag appears in the Artist + /// column on the iTunes Store. If the tag is not present, the iTunes Store + /// uses the contents of the tag. If is not present + /// at the RSS feed level, the iTunes Store uses the contents of the + /// tag. + public var iTunesAuthor: String? + + /// Specifying the tag with a Yes value in: + /// + /// - A tag (podcast), prevents the entire podcast from appearing on + /// the iTunes Store podcast directory + /// + /// - An tag (episode), prevents that episode from appearing on the + /// iTunes Store podcast directory + /// + /// For example, you might want to block a specific episode if you know that + /// its content would otherwise cause the entire podcast to be removed from + /// the iTunes Store. Specifying any value other than Yes has no effect. + public var iTunesBlock: String? + + /// Users can browse podcast subject categories in the iTunes Store by choosing + /// a category from the Podcasts pop-up menu in the navigation bar. Use the + /// tag to specify the browsing category for your podcast. + /// + /// You can also define a subcategory if one is available within your category. + /// Although you can specify more than one category and subcategory in your + /// feed, the iTunes Store only recognizes the first category and subcategory. + /// For a complete list of categories and subcategories, see Podcasts Connect + /// categories. + /// + /// Note: When specifying categories and subcategories, be sure to properly + /// escape ampersands: + /// + /// Single category: + /// + /// + /// Category with ampersand: + /// + /// + /// Category with subcategory: + /// + /// + /// + /// + /// Multiple categories: + /// + /// + /// + /// + /// + /// + public var iTunesCategories: [ITunesCategory]? + + /// Specify your podcast artwork using the attribute in the + /// tag. If you do not specify the tag, the + /// iTunes Store uses the content specified in the RSS feed image tag and Apple + /// does not consider your podcast for feature placement on the iTunes Store or + /// Podcasts. + /// + /// Depending on their device, subscribers see your podcast artwork in varying + /// sizes. Therefore, make sure your design is effective at both its original + /// size and at thumbnail size. Apple recommends including a title, brand, or + /// source name as part of your podcast artwork. For examples of podcast + /// artwork, see the Top Podcasts. To avoid technical issues when you update + /// your podcast artwork, be sure to: + /// + /// Change the artwork file name and URL at the same time + /// Verify the web server hosting your artwork allows HTTP head requests + /// The tag is also supported at the (episode) level. + /// For best results, Apple recommends embedding the same artwork within the + /// metadata for that episode's media file prior to uploading to your host + /// server; using Garageband or another content-creation tool to edit your + /// media file if needed. + /// + /// Note: Artwork must be a minimum size of 1400 x 1400 pixels and a maximum + /// size of 3000 x 3000 pixels, in JPEG or PNG format, 72 dpi, with appropriate + /// file extensions (.jpg, .png), and in the RGB colorspace. These requirements + /// are different from the standard RSS image tag specifications. + public var iTunesImage: ITunesImage? + + /// The content you specify in the tag appears in the Time + /// column in the List View on the iTunes Store. + /// + /// Specify one of the following formats for the tag value: + /// + /// HH:MM:SS + /// H:MM:SS + /// MM:SS + /// M:SS + /// + /// Where H = hours, M = minutes, and S = seconds. + /// + /// If you specify a single number as a value (without colons), the iTunes + /// Store displays the value as seconds. If you specify one colon, the iTunes + /// Store displays the number to the left as minutes and the number to the + /// right as seconds. If you specify more then two colons, the iTunes Store + /// ignores the numbers farthest to the right. + public var iTunesDuration: TimeInterval? + + /// The tag indicates whether your podcast contains explicit + /// material. You can specify the following values: + /// + /// Yes | Explicit | True. If you specify yes, explicit, or true, indicating + /// the presence of explicit content, the iTunes Store displays an Explicit + /// parental advisory graphic for your podcast. + /// Clean | No | False. If you specify clean, no, or false, indicating that + /// none of your podcast episodes contain explicit language or adult content, + /// the iTunes Store displays a Clean parental advisory graphic for your + /// podcast. + /// + /// Note: Podcasts containing explicit material are not available in some + /// iTunes Store territories. + public var iTunesExplicit: String? + + /// Specifying the tag with a Yes value indicates + /// that the video podcast episode is embedded with closed captioning and the + /// iTunes Store should display a closed-caption icon next to the corresponding + /// episode. This tag is only supported at the level (episode). + /// + /// Note: If you specify a value other than Yes, no closed-caption indicator + /// appears. + public var isClosedCaptioned: String? + + /// Use the tag to specify the number value in which you would + /// like the episode to appear and override the default ordering of episodes + /// on the iTunes Store. + /// + /// For example, if you want an to appear as the first episode of your + /// podcast, specify the tag with 1. If conflicting order + /// values are present in multiple episodes, the iTunes Store uses . + public var iTunesOrder: Int? + + /// Specifying the tag with a Yes value indicates that a + /// podcast is complete and you will not post any more episodes in the future. + /// This tag is only supported at the level (podcast). + /// + /// Note: If you specify a value other than Yes, nothing happens. + public var iTunesComplete: String? + + /// Use the tag to manually change the URL where your + /// podcast is located. This tag is only supported at a level + /// (podcast). + /// + /// http://newlocation.com/example.rss + /// Note: You should maintain your old feed until you have migrated your e + /// xisting subscribers. For more information, see Update your RSS feed URL. + public var iTunesNewFeedURL: String? + + /// Use the tag to specify contact information for the podcast + /// owner. Include the email address of the owner in a nested + /// tag and the name of the owner in a nested tag. + /// + /// The tag information is for administrative communication + /// about the podcast and is not displayed on the iTunes Store. + public var iTunesOwner: ITunesOwner? + + /// The content you specify in the tag appears in the + /// Description column on the iTunes Store. For best results, choose a subtitle + /// that is only a few words long. + public var iTunesSubtitle: String? + + /// The content you specify in the tag appears on the iTunes + /// Store page for your podcast. You can specify up to 4000 characters. The + /// information also appears in a separate window if a users clicks the + /// Information icon (Information icon) in the Description column. If you do + /// not specify a tag, the iTunes Store uses the information + /// in the tag. + public var iTunesSummary: String? + + /// Note: The keywords tag is deprecated by Apple and no longer documented in + /// the official list of tags. However many podcasts still use the tags and it + /// may be of use for developers building directory or search functionality so + /// it is included. + /// + /// + /// This tag allows users to search on text keywords. + /// Limited to 255 characters or less, plain text, no HTML, words must be + /// separated by spaces. + /// This tag is applicable to the Item element only. + public var iTunesKeywords: String? +} + +// MARK: - Equatable + +extension ITunesNamespace: Equatable { + + public static func ==(lhs: ITunesNamespace, rhs: ITunesNamespace) -> Bool { + return + lhs.iTunesAuthor == rhs.iTunesAuthor && + lhs.iTunesBlock == rhs.iTunesBlock && + lhs.iTunesCategories == rhs.iTunesCategories && + lhs.iTunesImage == rhs.iTunesImage && + lhs.iTunesDuration == rhs.iTunesDuration && + lhs.iTunesExplicit == rhs.iTunesExplicit && + lhs.isClosedCaptioned == rhs.isClosedCaptioned && + lhs.iTunesOrder == rhs.iTunesOrder && + lhs.iTunesComplete == rhs.iTunesComplete && + lhs.iTunesNewFeedURL == rhs.iTunesNewFeedURL && + lhs.iTunesOwner == rhs.iTunesOwner && + lhs.iTunesSubtitle == rhs.iTunesSubtitle && + lhs.iTunesSummary == rhs.iTunesSummary && + lhs.iTunesKeywords == rhs.iTunesKeywords + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesOwner.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesOwner.swift new file mode 100644 index 0000000..cf5709e --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesOwner.swift @@ -0,0 +1,53 @@ +// +// iTunesOwner.swift +// +// Copyright (c) 2017 Ben Murphy +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Use the tag to specify contact information for the podcast +/// owner. Include the email address of the owner in a nested tag +/// and the name of the owner in a nested tag. +/// +/// The tag information is for administrative communication about +/// the podcast and is not displayed on the iTunes Store. +public class ITunesOwner { + + /// The email address of the owner. + public var email: String? + + /// The name of the owner. + public var name: String? + +} + +// MARK: - Equatable + +extension ITunesOwner: Equatable { + + public static func ==(lhs: ITunesOwner, rhs: ITunesOwner) -> Bool { + return + lhs.email == rhs.email && + lhs.name == rhs.name + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesSubCategory.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesSubCategory.swift new file mode 100644 index 0000000..4195741 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesSubCategory.swift @@ -0,0 +1,117 @@ +// +// ITunesSubCategory.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Users can browse podcast subject categories in the iTunes Store by choosing +/// a category from the Podcasts pop-up menu in the navigation bar. Use the +/// tag to specify the browsing category for your podcast. +/// +/// You can also define a subcategory if one is available within your category. +/// Although you can specify more than one category and subcategory in your +/// feed, the iTunes Store only recognizes the first category and subcategory. +/// For a complete list of categories and subcategories, see Podcasts Connect +/// categories. +/// +/// Note: When specifying categories and subcategories, be sure to properly +/// escape ampersands: +/// +/// Single category: +/// +/// +/// Category with ampersand: +/// +/// +/// Category with subcategory: +/// +/// +/// +/// +/// Multiple categories: +/// +/// +/// +/// +/// +/// +public class ITunesSubCategory { + + /// The attributes of the element. + public class Attributes { + + /// The primary iTunes Category. + public var text: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + +} + +// MARK: - Initializers + +extension ITunesSubCategory { + + convenience init(attributes attributesDict: [String: String]) { + self.init() + self.attributes = ITunesSubCategory.Attributes(attributes: attributesDict) + } +} + +extension ITunesSubCategory.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.text = attributeDict["text"] + + } + +} + +// MARK: - Equatable + +extension ITunesSubCategory: Equatable { + + public static func ==(lhs: ITunesSubCategory, rhs: ITunesSubCategory) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension ITunesSubCategory.Attributes: Equatable { + + public static func ==(lhs: ITunesSubCategory.Attributes, rhs: ITunesSubCategory.Attributes) -> Bool { + return lhs.text == rhs.text + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RDFPath.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RDFPath.swift new file mode 100644 index 0000000..d0afa6e --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RDFPath.swift @@ -0,0 +1,89 @@ +// +// RDFPath.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Describes the individual path for each XML DOM element of an RDF feed +/// +/// See http://www.rssboard.org/rss-0-9-0 +enum RDFPath: String { + + case rdf = "/rdf:RDF" + case rdfChannel = "/rdf:RDF/channel" + case rdfChannelTitle = "/rdf:RDF/channel/title" + case rdfChannelLink = "/rdf:RDF/channel/link" + case rdfChannelDescription = "/rdf:RDF/channel/description" + case rdfChannelImage = "/rdf:RDF/channel/image" + case rdfChannelItems = "/rdf:RDF/channel/items" + case rdfChannelItemsRdfSeq = "/rdf:RDF/channel/items/rdf:Seq" + case rdfChannelItemsRdfSeqRdfLi = "/rdf:RDF/channel/items/rdf:Seq/rdf:li" + case rdfImage = "/rdf:RDF/image" + case rdfImageTitle = "/rdf:RDF/image/title" + case rdfImageURL = "/rdf:RDF/image/url" + case rdfImageLink = "/rdf:RDF/image/link" + case rdfItem = "/rdf:RDF/item" + case rdfItemTitle = "/rdf:RDF/item/title" + case rdfItemLink = "/rdf:RDF/item/link" + case rdfItemDescription = "/rdf:RDF/item/description" + + // Syndication + + case rdfChannelSyndicationUpdatePeriod = "/rdf:RDF/channel/sy:updatePeriod" + case rdfChannelSyndicationUpdateFrequency = "/rdf:RDF/channel/sy:updateFrequency" + case rdfChannelSyndicationUpdateBase = "/rdf:RDF/channel/sy:updateBase" + + // Dublin Core + + case rdfChannelDublinCoreTitle = "/rdf:RDF/channel/dc:title" + case rdfChannelDublinCoreCreator = "/rdf:RDF/channel/dc:creator" + case rdfChannelDublinCoreSubject = "/rdf:RDF/channel/dc:subject" + case rdfChannelDublinCoreDescription = "/rdf:RDF/channel/dc:description" + case rdfChannelDublinCorePublisher = "/rdf:RDF/channel/dc:publisher" + case rdfChannelDublinCoreContributor = "/rdf:RDF/channel/dc:contributor" + case rdfChannelDublinCoreDate = "/rdf:RDF/channel/dc:date" + case rdfChannelDublinCoreType = "/rdf:RDF/channel/dc:type" + case rdfChannelDublinCoreFormat = "/rdf:RDF/channel/dc:format" + case rdfChannelDublinCoreIdentifier = "/rdf:RDF/channel/dc:identifier" + case rdfChannelDublinCoreSource = "/rdf:RDF/channel/dc:source" + case rdfChannelDublinCoreLanguage = "/rdf:RDF/channel/dc:language" + case rdfChannelDublinCoreRelation = "/rdf:RDF/channel/dc:relation" + case rdfChannelDublinCoreCoverage = "/rdf:RDF/channel/dc:coverage" + case rdfChannelDublinCoreRights = "/rdf:RDF/channel/dc:rights" + case rdfItemDublinCoreTitle = "/rdf:RDF/item/dc:title" + case rdfItemDublinCoreCreator = "/rdf:RDF/item/dc:creator" + case rdfItemDublinCoreSubject = "/rdf:RDF/item/dc:subject" + case rdfItemDublinCoreDescription = "/rdf:RDF/item/dc:description" + case rdfItemDublinCorePublisher = "/rdf:RDF/item/dc:publisher" + case rdfItemDublinCoreContributor = "/rdf:RDF/item/dc:contributor" + case rdfItemDublinCoreDate = "/rdf:RDF/item/dc:date" + case rdfItemDublinCoreType = "/rdf:RDF/item/dc:type" + case rdfItemDublinCoreFormat = "/rdf:RDF/item/dc:format" + case rdfItemDublinCoreIdentifier = "/rdf:RDF/item/dc:identifier" + case rdfItemDublinCoreSource = "/rdf:RDF/item/dc:source" + case rdfItemDublinCoreLanguage = "/rdf:RDF/item/dc:language" + case rdfItemDublinCoreRelation = "/rdf:RDF/item/dc:relation" + case rdfItemDublinCoreCoverage = "/rdf:RDF/item/dc:coverage" + case rdfItemDublinCoreRights = "/rdf:RDF/item/dc:rights" + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed + mapAttributes.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed + mapAttributes.swift new file mode 100644 index 0000000..ed4c70b --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed + mapAttributes.swift @@ -0,0 +1,532 @@ +// +// RSSFeed + mapAttributes.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension RSSFeed { + + /// Maps the attributes of the specified dictionary for a given `RSSPath` + /// to the `RSSFeed` model, + /// + /// - Parameters: + /// - attributes: The attribute dictionary to map to the model. + /// - path: The path of feed's element. + func map(_ attributes: [String : String], for path: RSSPath) { + + switch path { + + case .rssChannelItem: + + if self.items == nil { + self.items = [] + } + + self.items?.append(RSSFeedItem()) + + case .rssChannelImage: + + if self.image == nil { + self.image = RSSFeedImage() + } + + case .rssChannelSkipDays: + + if self.skipDays == nil { + self.skipDays = [] + } + + case .rssChannelSkipHours: + + if self.skipHours == nil { + self.skipHours = [] + } + + case .rssChannelTextInput: + + if self.textInput == nil { + self.textInput = RSSFeedTextInput() + } + + case .rssChannelCategory: + + if self.categories == nil { + self.categories = [] + } + + self.categories?.append(RSSFeedCategory(attributes: attributes)) + + case .rssChannelCloud: + + if self.cloud == nil { + self.cloud = RSSFeedCloud(attributes: attributes) + } + + case .rssChannelItemCategory: + + if self.items?.last?.categories == nil { + self.items?.last?.categories = [] + } + + self.items?.last?.categories?.append(RSSFeedItemCategory(attributes: attributes)) + + case .rssChannelItemEnclosure: + + if self.items?.last?.enclosure == nil { + self.items?.last?.enclosure = RSSFeedItemEnclosure(attributes: attributes) + } + + case .rssChannelItemGUID: + + if self.items?.last?.guid == nil { + self.items?.last?.guid = RSSFeedItemGUID(attributes: attributes) + } + + case .rssChannelItemSource: + + if self.items?.last?.source == nil { + self.items?.last?.source = RSSFeedItemSource(attributes: attributes) + } + + case .rssChannelItemContentEncoded: + + if self.items?.last?.content == nil { + self.items?.last?.content = ContentNamespace() + } + + + case + .rssChannelSyndicationUpdateBase, + .rssChannelSyndicationUpdatePeriod, + .rssChannelSyndicationUpdateFrequency: + + if self.syndication == nil { + self.syndication = SyndicationNamespace() + } + + case + .rssChannelDublinCoreTitle, + .rssChannelDublinCoreCreator, + .rssChannelDublinCoreSubject, + .rssChannelDublinCoreDescription, + .rssChannelDublinCorePublisher, + .rssChannelDublinCoreContributor, + .rssChannelDublinCoreDate, + .rssChannelDublinCoreType, + .rssChannelDublinCoreFormat, + .rssChannelDublinCoreIdentifier, + .rssChannelDublinCoreSource, + .rssChannelDublinCoreLanguage, + .rssChannelDublinCoreRelation, + .rssChannelDublinCoreCoverage, + .rssChannelDublinCoreRights: + + if self.dublinCore == nil { + self.dublinCore = DublinCoreNamespace() + } + + case + .rssChannelItemDublinCoreTitle, + .rssChannelItemDublinCoreCreator, + .rssChannelItemDublinCoreSubject, + .rssChannelItemDublinCoreDescription, + .rssChannelItemDublinCorePublisher, + .rssChannelItemDublinCoreContributor, + .rssChannelItemDublinCoreDate, + .rssChannelItemDublinCoreType, + .rssChannelItemDublinCoreFormat, + .rssChannelItemDublinCoreIdentifier, + .rssChannelItemDublinCoreSource, + .rssChannelItemDublinCoreLanguage, + .rssChannelItemDublinCoreRelation, + .rssChannelItemDublinCoreCoverage, + .rssChannelItemDublinCoreRights: + + if self.items?.last?.dublinCore == nil { + self.items?.last?.dublinCore = DublinCoreNamespace() + } + + case + .rssChannelItunesAuthor, + .rssChannelItunesBlock, + .rssChannelItunesCategory, + .rssChannelItunesSubcategory, + .rssChannelItunesImage, + .rssChannelItunesExplicit, + .rssChannelItunesComplete, + .rssChannelItunesNewFeedURL, + .rssChannelItunesOwner, + .rssChannelItunesOwnerName, + .rssChannelItunesOwnerEmail, + .rssChannelItunesSubtitle, + .rssChannelItunesSummary, + .rssChannelItunesKeywords: + + if self.iTunes == nil { + self.iTunes = ITunesNamespace() + } + + switch path { + + case .rssChannelItunesCategory: + + if self.iTunes?.iTunesCategories == nil { + self.iTunes?.iTunesCategories = [] + } + + self.iTunes?.iTunesCategories?.append(ITunesCategory(attributes: attributes)) + + case .rssChannelItunesSubcategory: + + self.iTunes?.iTunesCategories?.last?.subcategory = ITunesSubCategory(attributes: attributes) + + case .rssChannelItunesImage: + + self.iTunes?.iTunesImage = ITunesImage(attributes: attributes) + + case .rssChannelItunesOwner: + + if self.iTunes?.iTunesOwner == nil { + self.iTunes?.iTunesOwner = ITunesOwner() + } + + default: break + + } + + case + .rssChannelItemItunesAuthor, + .rssChannelItemItunesBlock, + .rssChannelItemItunesDuration, + .rssChannelItemItunesImage, + .rssChannelItemItunesExplicit, + .rssChannelItemItunesIsClosedCaptioned, + .rssChannelItemItunesOrder, + .rssChannelItemItunesSubtitle, + .rssChannelItemItunesSummary, + .rssChannelItemItunesKeywords: + + if self.items?.last?.iTunes == nil { + self.items?.last?.iTunes = ITunesNamespace() + } + + switch path { + + case .rssChannelItemItunesImage: + + self.items?.last?.iTunes?.iTunesImage = ITunesImage(attributes: attributes) + + default: break + + } + + // MARK: Media + + case + .rssChannelItemMediaThumbnail, + .rssChannelItemMediaContent, + .rssChannelItemMediaCommunity, + .rssChannelItemMediaCommunityMediaStarRating, + .rssChannelItemMediaCommunityMediaStatistics, + .rssChannelItemMediaCommunityMediaTags, + .rssChannelItemMediaComments, + .rssChannelItemMediaCommentsMediaComment, + .rssChannelItemMediaEmbed, + .rssChannelItemMediaEmbedMediaParam, + .rssChannelItemMediaResponses, + .rssChannelItemMediaResponsesMediaResponse, + .rssChannelItemMediaBackLinks, + .rssChannelItemMediaBackLinksBackLink, + .rssChannelItemMediaStatus, + .rssChannelItemMediaPrice, + .rssChannelItemMediaLicense, + .rssChannelItemMediaSubTitle, + .rssChannelItemMediaPeerLink, + .rssChannelItemMediaLocation, + .rssChannelItemMediaLocationPosition, + .rssChannelItemMediaRestriction, + .rssChannelItemMediaScenes, + .rssChannelItemMediaScenesMediaScene, + .rssChannelItemMediaGroup, + .rssChannelItemMediaGroupMediaCategory, + .rssChannelItemMediaGroupMediaCredit, + .rssChannelItemMediaGroupMediaRating, + .rssChannelItemMediaGroupMediaContent: + + if self.items?.last?.media == nil { + self.items?.last?.media = MediaNamespace() + } + + switch path { + + case .rssChannelItemMediaThumbnail: + + if self.items?.last?.media?.mediaThumbnails == nil { + self.items?.last?.media?.mediaThumbnails = [] + } + + self.items?.last?.media?.mediaThumbnails?.append(MediaThumbnail(attributes: attributes)) + + case .rssChannelItemMediaContent: + + if self.items?.last?.media?.mediaContents == nil { + self.items?.last?.media?.mediaContents = [] + } + + self.items?.last?.media?.mediaContents?.append(MediaContent(attributes: attributes)) + + case .rssChannelItemMediaCommunity: + + if self.items?.last?.media?.mediaCommunity == nil { + self.items?.last?.media?.mediaCommunity = MediaCommunity() + } + + case .rssChannelItemMediaCommunityMediaStarRating: + + if self.items?.last?.media?.mediaCommunity?.mediaStarRating == nil { + self.items?.last?.media?.mediaCommunity?.mediaStarRating = MediaStarRating(attributes: attributes) + } + + case .rssChannelItemMediaCommunityMediaStatistics: + + if self.items?.last?.media?.mediaCommunity?.mediaStatistics == nil { + self.items?.last?.media?.mediaCommunity?.mediaStatistics = MediaStatistics(attributes: attributes) + } + + case .rssChannelItemMediaCommunityMediaTags: + + if self.items?.last?.media?.mediaCommunity?.mediaTags == nil { + self.items?.last?.media?.mediaCommunity?.mediaTags = [] + } + + case .rssChannelItemMediaComments: + + if self.items?.last?.media?.mediaComments == nil { + self.items?.last?.media?.mediaComments = [] + } + + case .rssChannelItemMediaEmbed: + + if self.items?.last?.media?.mediaEmbed == nil { + self.items?.last?.media?.mediaEmbed = MediaEmbed(attributes: attributes) + } + + case .rssChannelItemMediaEmbedMediaParam: + + if self.items?.last?.media?.mediaEmbed?.mediaParams == nil { + self.items?.last?.media?.mediaEmbed?.mediaParams = [] + } + + self.items?.last?.media?.mediaEmbed?.mediaParams?.append(MediaParam(attributes: attributes)) + + case .rssChannelItemMediaResponses: + + if self.items?.last?.media?.mediaResponses == nil { + self.items?.last?.media?.mediaResponses = [] + } + + case .rssChannelItemMediaBackLinks: + + if self.items?.last?.media?.mediaBackLinks == nil { + self.items?.last?.media?.mediaBackLinks = [] + } + + case .rssChannelItemMediaStatus: + + if self.items?.last?.media?.mediaStatus == nil { + self.items?.last?.media?.mediaStatus = MediaStatus(attributes: attributes) + } + + case .rssChannelItemMediaPrice: + + if self.items?.last?.media?.mediaPrices == nil { + self.items?.last?.media?.mediaPrices = [] + } + + self.items?.last?.media?.mediaPrices?.append(MediaPrice(attributes: attributes)) + + case .rssChannelItemMediaLicense: + + if self.items?.last?.media?.mediaLicense == nil { + self.items?.last?.media?.mediaLicense = MediaLicence(attributes: attributes) + } + + case .rssChannelItemMediaSubTitle: + + if self.items?.last?.media?.mediaSubTitle == nil { + self.items?.last?.media?.mediaSubTitle = MediaSubTitle(attributes: attributes) + } + + case .rssChannelItemMediaPeerLink: + + if self.items?.last?.media?.mediaPeerLink == nil { + self.items?.last?.media?.mediaPeerLink = MediaPeerLink(attributes: attributes) + } + + case .rssChannelItemMediaLocation: + + if self.items?.last?.media?.mediaLocation == nil { + self.items?.last?.media?.mediaLocation = MediaLocation(attributes: attributes) + } + + case .rssChannelItemMediaRestriction: + + if self.items?.last?.media?.mediaRestriction == nil { + self.items?.last?.media?.mediaRestriction = MediaRestriction(attributes: attributes) + } + + case .rssChannelItemMediaScenes: + + if self.items?.last?.media?.mediaScenes == nil { + self.items?.last?.media?.mediaScenes = [] + } + + case .rssChannelItemMediaScenesMediaScene: + + if self.items?.last?.media?.mediaScenes == nil { + self.items?.last?.media?.mediaScenes = [] + } + + self.items?.last?.media?.mediaScenes?.append(MediaScene()) + + case .rssChannelItemMediaGroup: + + if self.items?.last?.media?.mediaGroup == nil { + self.items?.last?.media?.mediaGroup = MediaGroup() + } + + case .rssChannelItemMediaGroupMediaCategory: + + if self.items?.last?.media?.mediaGroup?.mediaCategory == nil { + self.items?.last?.media?.mediaGroup?.mediaCategory = MediaCategory(attributes: attributes) + } + + case .rssChannelItemMediaGroupMediaCredit: + + if self.items?.last?.media?.mediaGroup?.mediaCredits == nil { + self.items?.last?.media?.mediaGroup?.mediaCredits = [] + } + + self.items?.last?.media?.mediaGroup?.mediaCredits?.append(MediaCredit(attributes: attributes)) + + case .rssChannelItemMediaGroupMediaRating: + + if self.items?.last?.media?.mediaGroup?.mediaRating == nil { + self.items?.last?.media?.mediaGroup?.mediaRating = MediaRating(attributes: attributes) + } + + case .rssChannelItemMediaGroupMediaContent: + + if self.items?.last?.media?.mediaGroup?.mediaContents == nil { + self.items?.last?.media?.mediaGroup?.mediaContents = [] + } + + self.items?.last?.media?.mediaGroup?.mediaContents?.append(MediaContent(attributes: attributes)) + + default: break + + } + + + default: break + + + } + + + } + + /// Maps the attributes of the specified dictionary for a given `RSSPath` + /// to the `RSSFeed` model, + /// + /// - Parameters: + /// - attributes: The attribute dictionary to map to the model. + /// - path: The path of feed's element. + func map(_ attributes: [String : String], for path: RDFPath) { + + switch path { + + case .rdfItem: + if self.items == nil { + self.items = [] + } + + self.items?.append(RSSFeedItem()) + + case + .rdfChannelSyndicationUpdateBase, + .rdfChannelSyndicationUpdatePeriod, + .rdfChannelSyndicationUpdateFrequency: + + if self.syndication == nil { + self.syndication = SyndicationNamespace() + } + + case + .rdfChannelDublinCoreTitle, + .rdfChannelDublinCoreCreator, + .rdfChannelDublinCoreSubject, + .rdfChannelDublinCoreDescription, + .rdfChannelDublinCorePublisher, + .rdfChannelDublinCoreContributor, + .rdfChannelDublinCoreDate, + .rdfChannelDublinCoreType, + .rdfChannelDublinCoreFormat, + .rdfChannelDublinCoreIdentifier, + .rdfChannelDublinCoreSource, + .rdfChannelDublinCoreLanguage, + .rdfChannelDublinCoreRelation, + .rdfChannelDublinCoreCoverage, + .rdfChannelDublinCoreRights: + + if self.dublinCore == nil { + self.dublinCore = DublinCoreNamespace() + } + + case + .rdfItemDublinCoreTitle, + .rdfItemDublinCoreCreator, + .rdfItemDublinCoreSubject, + .rdfItemDublinCoreDescription, + .rdfItemDublinCorePublisher, + .rdfItemDublinCoreContributor, + .rdfItemDublinCoreDate, + .rdfItemDublinCoreType, + .rdfItemDublinCoreFormat, + .rdfItemDublinCoreIdentifier, + .rdfItemDublinCoreSource, + .rdfItemDublinCoreLanguage, + .rdfItemDublinCoreRelation, + .rdfItemDublinCoreCoverage, + .rdfItemDublinCoreRights: + + if self.items?.last?.dublinCore == nil { + self.items?.last?.dublinCore = DublinCoreNamespace() + } + + default: break + } + + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed + mapCharacters.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed + mapCharacters.swift new file mode 100644 index 0000000..e5e849f --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed + mapCharacters.swift @@ -0,0 +1,205 @@ +// +// RSSFeed + mapCharacters.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension RSSFeed { + + /// Maps the characters in the specified string to the `RSSFeed` model. + /// + /// - Parameters: + /// - string: The string to map to the model. + /// - path: The path of feed's element. + func map(_ string: String, for path: RSSPath) { + + switch path { + case .rssChannelTitle: self.title = self.title?.appending(string) ?? string + case .rssChannelLink: self.link = self.link?.appending(string) ?? string + case .rssChannelDescription: self.description = self.description?.appending(string) ?? string + case .rssChannelLanguage: self.language = self.language?.appending(string) ?? string + case .rssChannelCopyright: self.copyright = self.copyright?.appending(string) ?? string + case .rssChannelManagingEditor: self.managingEditor = self.managingEditor?.appending(string) ?? string + case .rssChannelWebMaster: self.webMaster = self.webMaster?.appending(string) ?? string + case .rssChannelPubDate: self.pubDate = string.toDate(from: .rfc822) + case .rssChannelLastBuildDate: self.lastBuildDate = string.toDate(from: .rfc822) + case .rssChannelCategory: self.categories?.last?.value = self.categories?.last?.value?.appending(string) ?? string + case .rssChannelGenerator: self.generator = self.generator?.appending(string) ?? string + case .rssChannelDocs: self.docs = self.docs?.appending(string) ?? string + case .rssChannelRating: self.rating = self.rating?.appending(string) ?? string + case .rssChannelTTL: self.ttl = Int(string) + case .rssChannelImageURL: self.image?.url = self.image?.url?.appending(string) ?? string + case .rssChannelImageTitle: self.image?.title = self.image?.title?.appending(string) ?? string + case .rssChannelImageLink: self.image?.link = self.image?.link?.appending(string) ?? string + case .rssChannelImageWidth: self.image?.width = Int(string) + case .rssChannelImageHeight: self.image?.height = Int(string) + case .rssChannelImageDescription: self.image?.description = self.image?.description?.appending(string) ?? string + case .rssChannelTextInputTitle: self.textInput?.title = self.textInput?.title?.appending(string) ?? string + case .rssChannelTextInputDescription: self.textInput?.description = self.textInput?.description?.appending(string) ?? string + case .rssChannelTextInputName: self.textInput?.name = self.textInput?.name?.appending(string) ?? string + case .rssChannelTextInputLink: self.textInput?.link = self.textInput?.link?.appending(string) ?? string + case .rssChannelSkipHoursHour: + if let hour = RSSFeedSkipHour(string), 0...23 ~= hour { + self.skipHours?.append(hour) + } + case .rssChannelSkipDaysDay: + if let day = RSSFeedSkipDay(rawValue: string) { + self.skipDays?.append(day) + } + case .rssChannelItemTitle: self.items?.last?.title = self.items?.last?.title?.appending(string) ?? string + case .rssChannelItemLink: self.items?.last?.link = self.items?.last?.link?.appending(string) ?? string + case .rssChannelItemDescription: self.items?.last?.description = self.items?.last?.description?.appending(string) ?? string + case .rssChannelItemAuthor: self.items?.last?.author = self.items?.last?.author?.appending(string) ?? string + case .rssChannelItemCategory: self.items?.last?.categories?.last?.value = self.items?.last?.categories?.last?.value?.appending(string) ?? string + case .rssChannelItemComments: self.items?.last?.comments = self.items?.last?.comments?.appending(string) ?? string + case .rssChannelItemGUID: self.items?.last?.guid?.value = self.items?.last?.guid?.value?.appending(string) ?? string + case .rssChannelItemPubDate: self.items?.last?.pubDate = string.toDate(from: .rfc822) + case .rssChannelItemSource: self.items?.last?.source?.value = self.items?.last?.source?.value?.appending(string) ?? string + case .rssChannelItemContentEncoded: self.items?.last?.content?.contentEncoded = self.items?.last?.content?.contentEncoded?.appending(string) ?? string + case .rssChannelSyndicationUpdatePeriod: self.syndication?.syUpdatePeriod = SyndicationUpdatePeriod(rawValue: string) + case .rssChannelSyndicationUpdateFrequency: self.syndication?.syUpdateFrequency = Int(string) + case .rssChannelSyndicationUpdateBase: self.syndication?.syUpdateBase = string.toDate(from: .iso8601) + case .rssChannelDublinCoreTitle: self.dublinCore?.dcTitle = self.dublinCore?.dcTitle?.appending(string) ?? string + case .rssChannelDublinCoreCreator: self.dublinCore?.dcCreator = self.dublinCore?.dcCreator?.appending(string) ?? string + case .rssChannelDublinCoreSubject: self.dublinCore?.dcSubject = self.dublinCore?.dcSubject?.appending(string) ?? string + case .rssChannelDublinCoreDescription: self.dublinCore?.dcDescription = self.dublinCore?.dcDescription?.appending(string) ?? string + case .rssChannelDublinCorePublisher: self.dublinCore?.dcPublisher = self.dublinCore?.dcPublisher?.appending(string) ?? string + case .rssChannelDublinCoreContributor: self.dublinCore?.dcContributor = self.dublinCore?.dcContributor?.appending(string) ?? string + case .rssChannelDublinCoreDate: self.dublinCore?.dcDate = string.toDate(from: .iso8601) + case .rssChannelDublinCoreType: self.dublinCore?.dcType = self.dublinCore?.dcType?.appending(string) ?? string + case .rssChannelDublinCoreFormat: self.dublinCore?.dcFormat = self.dublinCore?.dcFormat?.appending(string) ?? string + case .rssChannelDublinCoreIdentifier: self.dublinCore?.dcIdentifier = self.dublinCore?.dcIdentifier?.appending(string) ?? string + case .rssChannelDublinCoreSource: self.dublinCore?.dcSource = self.dublinCore?.dcSource?.appending(string) ?? string + case .rssChannelDublinCoreLanguage: self.dublinCore?.dcLanguage = self.dublinCore?.dcLanguage?.appending(string) ?? string + case .rssChannelDublinCoreRelation: self.dublinCore?.dcRelation = self.dublinCore?.dcRelation?.appending(string) ?? string + case .rssChannelDublinCoreCoverage: self.dublinCore?.dcCoverage = self.dublinCore?.dcCoverage?.appending(string) ?? string + case .rssChannelDublinCoreRights: self.dublinCore?.dcRights = self.dublinCore?.dcRights?.appending(string) ?? string + case .rssChannelItemDublinCoreTitle: self.items?.last?.dublinCore?.dcTitle = self.items?.last?.dublinCore?.dcTitle?.appending(string) ?? string + case .rssChannelItemDublinCoreCreator: self.items?.last?.dublinCore?.dcCreator = self.items?.last?.dublinCore?.dcCreator?.appending(string) ?? string + case .rssChannelItemDublinCoreSubject: self.items?.last?.dublinCore?.dcSubject = self.items?.last?.dublinCore?.dcSubject?.appending(string) ?? string + case .rssChannelItemDublinCoreDescription: self.items?.last?.dublinCore?.dcDescription = self.items?.last?.dublinCore?.dcDescription?.appending(string) ?? string + case .rssChannelItemDublinCorePublisher: self.items?.last?.dublinCore?.dcPublisher = self.items?.last?.dublinCore?.dcPublisher?.appending(string) ?? string + case .rssChannelItemDublinCoreContributor: self.items?.last?.dublinCore?.dcContributor = self.items?.last?.dublinCore?.dcContributor?.appending(string) ?? string + case .rssChannelItemDublinCoreDate: self.items?.last?.dublinCore?.dcDate = string.toDate(from: .iso8601) + case .rssChannelItemDublinCoreType: self.items?.last?.dublinCore?.dcType = self.items?.last?.dublinCore?.dcType?.appending(string) ?? string + case .rssChannelItemDublinCoreFormat: self.items?.last?.dublinCore?.dcFormat = self.items?.last?.dublinCore?.dcFormat?.appending(string) ?? string + case .rssChannelItemDublinCoreIdentifier: self.items?.last?.dublinCore?.dcIdentifier = self.items?.last?.dublinCore?.dcIdentifier?.appending(string) ?? string + case .rssChannelItemDublinCoreSource: self.items?.last?.dublinCore?.dcSource = self.items?.last?.dublinCore?.dcSource?.appending(string) ?? string + case .rssChannelItemDublinCoreLanguage: self.items?.last?.dublinCore?.dcLanguage = self.items?.last?.dublinCore?.dcLanguage?.appending(string) ?? string + case .rssChannelItemDublinCoreRelation: self.items?.last?.dublinCore?.dcRelation = self.items?.last?.dublinCore?.dcRelation?.appending(string) ?? string + case .rssChannelItemDublinCoreCoverage: self.items?.last?.dublinCore?.dcCoverage = self.items?.last?.dublinCore?.dcCoverage?.appending(string) ?? string + case .rssChannelItemDublinCoreRights: self.items?.last?.dublinCore?.dcRights = self.items?.last?.dublinCore?.dcRights?.appending(string) ?? string + case .rssChannelItunesAuthor: self.iTunes?.iTunesAuthor = self.iTunes?.iTunesAuthor?.appending(string) ?? string + case .rssChannelItunesBlock: self.iTunes?.iTunesBlock = self.iTunes?.iTunesBlock?.appending(string) ?? string + case .rssChannelItunesExplicit: self.iTunes?.iTunesExplicit = self.iTunes?.iTunesExplicit?.appending(string) ?? string + case .rssChannelItunesComplete: self.iTunes?.iTunesComplete = self.iTunes?.iTunesComplete?.appending(string) ?? string + case .rssChannelItunesNewFeedURL: self.iTunes?.iTunesNewFeedURL = self.iTunes?.iTunesNewFeedURL?.appending(string) ?? string + case .rssChannelItunesOwnerName: self.iTunes?.iTunesOwner?.name = self.iTunes?.iTunesOwner?.name?.appending(string) ?? string + case .rssChannelItunesOwnerEmail: self.iTunes?.iTunesOwner?.email = self.iTunes?.iTunesOwner?.email?.appending(string) ?? string + case .rssChannelItunesSubtitle: self.iTunes?.iTunesSubtitle = self.iTunes?.iTunesSubtitle?.appending(string) ?? string + case .rssChannelItunesSummary: self.iTunes?.iTunesSummary = self.iTunes?.iTunesSummary?.appending(string) ?? string + case .rssChannelItunesKeywords: self.iTunes?.iTunesKeywords = self.iTunes?.iTunesKeywords?.appending(string) ?? string + case .rssChannelItemItunesAuthor: self.items?.last?.iTunes?.iTunesAuthor = self.items?.last?.iTunes?.iTunesAuthor?.appending(string) ?? string + case .rssChannelItemItunesBlock: self.items?.last?.iTunes?.iTunesBlock = self.items?.last?.iTunes?.iTunesBlock?.appending(string) ?? string + case .rssChannelItemItunesDuration: self.items?.last?.iTunes?.iTunesDuration = string.toDuration() + case .rssChannelItemItunesExplicit: self.items?.last?.iTunes?.iTunesExplicit = self.items?.last?.iTunes?.iTunesExplicit?.appending(string) ?? string + case .rssChannelItemItunesIsClosedCaptioned: self.items?.last?.iTunes?.isClosedCaptioned = self.items?.last?.iTunes?.isClosedCaptioned?.appending(string) ?? string + case .rssChannelItemItunesOrder: self.items?.last?.iTunes?.iTunesOrder = Int(string) + case .rssChannelItemItunesSubtitle: self.items?.last?.iTunes?.iTunesSubtitle = self.items?.last?.iTunes?.iTunesSubtitle?.appending(string) ?? string + case .rssChannelItemItunesSummary: self.items?.last?.iTunes?.iTunesSummary = self.items?.last?.iTunes?.iTunesSummary?.appending(string) ?? string + case .rssChannelItemItunesKeywords: self.items?.last?.iTunes?.iTunesKeywords = self.items?.last?.iTunes?.iTunesKeywords?.appending(string) ?? string + case .rssChannelItemMediaThumbnail: self.items?.last?.media?.mediaThumbnails?.last?.value = self.items?.last?.media?.mediaThumbnails?.last?.value?.appending(string) ?? string + case .rssChannelItemMediaLicense: self.items?.last?.media?.mediaLicense?.value = self.items?.last?.media?.mediaLicense?.value?.appending(string) ?? string + case .rssChannelItemMediaRestriction: self.items?.last?.media?.mediaRestriction?.value = self.items?.last?.media?.mediaRestriction?.value?.appending(string) ?? string + case .rssChannelItemMediaCommunityMediaTags: self.items?.last?.media?.mediaCommunity?.mediaTags = MediaTag.tagsFrom(string: string) + case .rssChannelItemMediaCommentsMediaComment: self.items?.last?.media?.mediaComments?.append(string) + case .rssChannelItemMediaEmbedMediaParam: self.items?.last?.media?.mediaEmbed?.mediaParams?.last?.value = self.items?.last?.media?.mediaEmbed?.mediaParams?.last?.value?.appending(string) ?? string + case .rssChannelItemMediaGroupMediaCredit: self.items?.last?.media?.mediaGroup?.mediaCredits?.last?.value = self.items?.last?.media?.mediaGroup?.mediaCredits?.last?.value?.appending(string) ?? string + case .rssChannelItemMediaGroupMediaCategory: self.items?.last?.media?.mediaGroup?.mediaCategory?.value = self.items?.last?.media?.mediaGroup?.mediaCategory?.value?.appending(string) ?? string + case .rssChannelItemMediaGroupMediaRating: self.items?.last?.media?.mediaGroup?.mediaRating?.value = self.items?.last?.media?.mediaGroup?.mediaRating?.value?.appending(string) ?? string + case .rssChannelItemMediaResponsesMediaResponse: self.items?.last?.media?.mediaResponses?.append(string) + case .rssChannelItemMediaBackLinksBackLink: self.items?.last?.media?.mediaBackLinks?.append(string) + case .rssChannelItemMediaLocationPosition: self.items?.last?.media?.mediaLocation?.mapFrom(latLng: string) + case .rssChannelItemMediaScenesMediaSceneSceneTitle: self.items?.last?.media?.mediaScenes?.last?.sceneTitle = self.items?.last?.media?.mediaScenes?.last?.sceneTitle?.appending(string) ?? string + case .rssChannelItemMediaScenesMediaSceneSceneDescription: self.items?.last?.media?.mediaScenes?.last?.sceneDescription = self.items?.last?.media?.mediaScenes?.last?.sceneDescription?.appending(string) ?? string + case .rssChannelItemMediaScenesMediaSceneSceneStartTime: self.items?.last?.media?.mediaScenes?.last?.sceneStartTime = string.toDuration() + case .rssChannelItemMediaScenesMediaSceneSceneEndTime: self.items?.last?.media?.mediaScenes?.last?.sceneEndTime = string.toDuration() + default: break + } + + } + + /// Maps the characters in the specified string to the `RSSFeed` model. + /// + /// - Parameters: + /// - string: The string to map to the model. + /// - path: The path of feed's element. + func map(_ string: String, for path: RDFPath) { + + switch path { + case .rdfChannelTitle: self.title = self.title?.appending(string) ?? string + case .rdfChannelLink: self.link = self.link?.appending(string) ?? string + case .rdfChannelDescription: self.description = self.description?.appending(string) ?? string + case .rdfChannelImage: self.image?.url = self.image?.url?.appending(string) ?? string + case .rdfItemTitle: self.items?.last?.title = self.items?.last?.title?.appending(string) ?? string + case .rdfItemLink: self.items?.last?.link = self.items?.last?.link?.appending(string) ?? string + case .rdfItemDescription: self.items?.last?.description = self.items?.last?.description?.appending(string) ?? string + case .rdfChannelSyndicationUpdatePeriod: self.syndication?.syUpdatePeriod = SyndicationUpdatePeriod(rawValue: string) + case .rdfChannelSyndicationUpdateFrequency: self.syndication?.syUpdateFrequency = Int(string) + case .rdfChannelSyndicationUpdateBase: self.syndication?.syUpdateBase = string.toDate(from: .iso8601) + case .rdfChannelDublinCoreTitle: self.dublinCore?.dcTitle = self.dublinCore?.dcTitle?.appending(string) ?? string + case .rdfChannelDublinCoreCreator: self.dublinCore?.dcCreator = self.dublinCore?.dcCreator?.appending(string) ?? string + case .rdfChannelDublinCoreSubject: self.dublinCore?.dcSubject = self.dublinCore?.dcSubject?.appending(string) ?? string + case .rdfChannelDublinCoreDescription: self.dublinCore?.dcDescription = self.dublinCore?.dcDescription?.appending(string) ?? string + case .rdfChannelDublinCorePublisher: self.dublinCore?.dcPublisher = self.dublinCore?.dcPublisher?.appending(string) ?? string + case .rdfChannelDublinCoreContributor: self.dublinCore?.dcContributor = self.dublinCore?.dcContributor?.appending(string) ?? string + case .rdfChannelDublinCoreDate: self.dublinCore?.dcDate = string.toDate(from: .iso8601) + case .rdfChannelDublinCoreType: self.dublinCore?.dcType = self.dublinCore?.dcType?.appending(string) ?? string + case .rdfChannelDublinCoreFormat: self.dublinCore?.dcFormat = self.dublinCore?.dcFormat?.appending(string) ?? string + case .rdfChannelDublinCoreIdentifier: self.dublinCore?.dcIdentifier = self.dublinCore?.dcIdentifier?.appending(string) ?? string + case .rdfChannelDublinCoreSource: self.dublinCore?.dcSource = self.dublinCore?.dcSource?.appending(string) ?? string + case .rdfChannelDublinCoreLanguage: self.dublinCore?.dcLanguage = self.dublinCore?.dcLanguage?.appending(string) ?? string + case .rdfChannelDublinCoreRelation: self.dublinCore?.dcRelation = self.dublinCore?.dcRelation?.appending(string) ?? string + case .rdfChannelDublinCoreCoverage: self.dublinCore?.dcCoverage = self.dublinCore?.dcCoverage?.appending(string) ?? string + case .rdfChannelDublinCoreRights: self.dublinCore?.dcRights = self.dublinCore?.dcRights?.appending(string) ?? string + case .rdfItemDublinCoreTitle: self.items?.last?.dublinCore?.dcTitle = self.items?.last?.dublinCore?.dcTitle?.appending(string) ?? string + case .rdfItemDublinCoreCreator: self.items?.last?.dublinCore?.dcCreator = self.items?.last?.dublinCore?.dcCreator?.appending(string) ?? string + case .rdfItemDublinCoreSubject: self.items?.last?.dublinCore?.dcSubject = self.items?.last?.dublinCore?.dcSubject?.appending(string) ?? string + case .rdfItemDublinCoreDescription: self.items?.last?.dublinCore?.dcDescription = self.items?.last?.dublinCore?.dcDescription?.appending(string) ?? string + case .rdfItemDublinCorePublisher: self.items?.last?.dublinCore?.dcPublisher = self.items?.last?.dublinCore?.dcPublisher?.appending(string) ?? string + case .rdfItemDublinCoreContributor: self.items?.last?.dublinCore?.dcContributor = self.items?.last?.dublinCore?.dcContributor?.appending(string) ?? string + case .rdfItemDublinCoreDate: self.items?.last?.dublinCore?.dcDate = string.toDate(from: .iso8601) + case .rdfItemDublinCoreType: self.items?.last?.dublinCore?.dcType = self.items?.last?.dublinCore?.dcType?.appending(string) ?? string + case .rdfItemDublinCoreFormat: self.items?.last?.dublinCore?.dcFormat = self.items?.last?.dublinCore?.dcFormat?.appending(string) ?? string + case .rdfItemDublinCoreIdentifier: self.items?.last?.dublinCore?.dcIdentifier = self.items?.last?.dublinCore?.dcIdentifier?.appending(string) ?? string + case .rdfItemDublinCoreSource: self.items?.last?.dublinCore?.dcSource = self.items?.last?.dublinCore?.dcSource?.appending(string) ?? string + case .rdfItemDublinCoreLanguage: self.items?.last?.dublinCore?.dcLanguage = self.items?.last?.dublinCore?.dcLanguage?.appending(string) ?? string + case .rdfItemDublinCoreRelation: self.items?.last?.dublinCore?.dcRelation = self.items?.last?.dublinCore?.dcRelation?.appending(string) ?? string + case .rdfItemDublinCoreCoverage: self.items?.last?.dublinCore?.dcCoverage = self.items?.last?.dublinCore?.dcCoverage?.appending(string) ?? string + case .rdfItemDublinCoreRights: self.items?.last?.dublinCore?.dcRights = self.items?.last?.dublinCore?.dcRights?.appending(string) ?? string + default: break + } + + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed.swift new file mode 100755 index 0000000..37bee5c --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed.swift @@ -0,0 +1,286 @@ +// +// RSSFeed.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Data model for the XML DOM of the RSS 2.0 Specification +/// See http://cyber.law.harvard.edu/rss/rss.html +/// +/// At the top level, a RSS document is a element, with a mandatory +/// attribute called version, that specifies the version of RSS that the +/// document conforms to. If it conforms to this specification, the version +/// attribute must be 2.0. +/// +/// Subordinate to the element is a single element, which +/// contains information about the channel (metadata) and its contents. +public class RSSFeed { + + /// The name of the channel. It's how people refer to your service. If + /// you have an HTML website that contains the same information as your + /// RSS file, the title of your channel should be the same as the title + /// of your website. + /// + /// Example: GoUpstate.com News Headlines + public var title: String? + + /// The URL to the HTML website corresponding to the channel. + /// + /// Example: http://www.goupstate.com/ + public var link: String? + + /// Phrase or sentence describing the channel. + /// + /// Example: The latest news from GoUpstate.com, a Spartanburg Herald-Journal + /// Web site. + public var description: String? + + /// The language the channel is written in. This allows aggregators to group + /// all Italian language sites, for example, on a single page. A list of + /// allowable values for this element, as provided by Netscape, is here: + /// http://cyber.law.harvard.edu/rss/languages.html + /// + /// You may also use values defined by the W3C: + /// http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes + /// + /// Example: en-us + public var language: String? + + /// Copyright notice for content in the channel. + /// + /// Example: Copyright 2002, Spartanburg Herald-Journal + public var copyright: String? + + /// Email address for person responsible for editorial content. + /// + /// Example: geo@herald.com (George Matesky) + public var managingEditor: String? + + /// Email address for person responsible for technical issues relating to + /// channel. + /// + /// Example: betty@herald.com (Betty Guernsey) + public var webMaster: String? + + /// The publication date for the content in the channel. For example, the + /// New York Times publishes on a daily basis, the publication date flips + /// once every 24 hours. That's when the pubDate of the channel changes. + /// All date-times in RSS conform to the Date and Time Specification of + /// RFC 822, with the exception that the year may be expressed with two + /// characters or four characters (four preferred). + /// + /// Example: Sat, 07 Sep 2002 00:00:01 GMT + public var pubDate: Date? + + /// The last time the content of the channel changed. + /// + /// Example: Sat, 07 Sep 2002 09:42:31 GMT + public var lastBuildDate: Date? + + /// Specify one or more categories that the channel belongs to. Follows the + /// same rules as the -level category element. + /// + /// Example: Newspapers + public var categories: [RSSFeedCategory]? + + /// A string indicating the program used to generate the channel. + /// + /// Example: MightyInHouse Content System v2.3 + public var generator: String? + + /// A URL that points to the documentation for the format used in the RSS + /// file. It's probably a pointer to this page. It's for people who might + /// stumble across an RSS file on a Web server 25 years from now and wonder + /// what it is. + /// + /// Example: http://blogs.law.harvard.edu/tech/rss + public var docs: String? + + /// Allows processes to register with a cloud to be notified of updates to + /// the channel, implementing a lightweight publish-subscribe protocol for + /// RSS feeds. + /// + /// Example: + /// + /// is an optional sub-element of . + /// + /// It specifies a web service that supports the rssCloud interface which can + /// be implemented in HTTP-POST, XML-RPC or SOAP 1.1. + /// + /// Its purpose is to allow processes to register with a cloud to be notified + /// of updates to the channel, implementing a lightweight publish-subscribe + /// protocol for RSS feeds. + /// + /// + /// + /// In this example, to request notification on the channel it appears in, + /// you would send an XML-RPC message to rpc.sys.com on port 80, with a path + /// of /RPC2. The procedure to call is myCloud.rssPleaseNotify. + /// + /// A full explanation of this element and the rssCloud interface is here: + /// http://cyber.law.harvard.edu/rss/soapMeetsRss.html#rsscloudInterface + public var cloud: RSSFeedCloud? + + /// The PICS rating for the channel. + public var rating: String? + + /// ttl stands for time to live. It's a number of minutes that indicates how + /// long a channel can be cached before refreshing from the source. + /// + /// Example: 60 + /// + /// is an optional sub-element of . + /// + /// ttl stands for time to live. It's a number of minutes that indicates how + /// long a channel can be cached before refreshing from the source. This makes + /// it possible for RSS sources to be managed by a file-sharing network such + /// as Gnutella. + public var ttl: Int? + + /// Specifies a GIF, JPEG or PNG image that can be displayed with the channel. + /// + /// is an optional sub-element of , which contains three + /// required and three optional sub-elements. + /// + /// is the URL of a GIF, JPEG or PNG image that represents the channel. + /// + /// describes the image, it's used in the ALT attribute of the HTML + /// <img> tag when the channel is rendered in HTML. + /// + /// <link> is the URL of the site, when the channel is rendered, the image + /// is a link to the site. (Note, in practice the image <title> and <link> + /// should have the same value as the channel's <title> and <link>. + /// + /// Optional elements include <width> and <height>, numbers, indicating the + /// width and height of the image in pixels. <description> contains text + /// that is included in the TITLE attribute of the link formed around the + /// image in the HTML rendering. + /// + /// Maximum value for width is 144, default value is 88. + /// + /// Maximum value for height is 400, default value is 31. + public var image: RSSFeedImage? + + /// Specifies a text input box that can be displayed with the channel. + /// + /// A channel may optionally contain a <textInput> sub-element, which contains + /// four required sub-elements. + /// + /// <title> -- The label of the Submit button in the text input area. + /// + /// <description> -- Explains the text input area. + /// + /// <name> -- The name of the text object in the text input area. + /// + /// <link> -- The URL of the CGI script that processes text input requests. + /// + /// The purpose of the <textInput> element is something of a mystery. You can + /// use it to specify a search engine box. Or to allow a reader to provide + /// feedback. Most aggregators ignore it. + public var textInput: RSSFeedTextInput? + + /// A hint for aggregators telling them which hours they can skip. + /// + /// An XML element that contains up to 24 <hour> sub-elements whose value is a + /// number between 0 and 23, representing a time in GMT, when aggregators, if they + /// support the feature, may not read the channel on hours listed in the skipHours + /// element. + /// + /// The hour beginning at midnight is hour zero. + public var skipHours: [RSSFeedSkipHour]? + + /// A hint for aggregators telling them which days they can skip. + /// + /// An XML element that contains up to seven <day> sub-elements whose value + /// is Monday, Tuesday, Wednesday, Thursday, Friday, Saturday or Sunday. + /// Aggregators may not read the channel during days listed in the skipDays + /// element. + public var skipDays: [RSSFeedSkipDay]? + + /// A channel may contain any number of <item>s. An item may represent a + /// "story" -- much like a story in a newspaper or magazine; if so its + /// description is a synopsis of the story, and the link points to the full + /// story. An item may also be complete in itself, if so, the description + /// contains the text (entity-encoded HTML is allowed; see examples: + /// http://cyber.law.harvard.edu/rss/encodingDescriptions.html), and + /// the link and title may be omitted. All elements of an item are optional, + /// however at least one of title or description must be present. + public var items: [RSSFeedItem]? + + + // MARK: - Namespaces + + /// The Dublin Core Metadata Element Set is a standard for cross-domain + /// resource description. + /// + /// See https://tools.ietf.org/html/rfc5013 + public var dublinCore: DublinCoreNamespace? + + /// Provides syndication hints to aggregators and others picking up this RDF Site + /// Summary (RSS) feed regarding how often it is updated. For example, if you + /// updated your file twice an hour, updatePeriod would be "hourly" and + /// updateFrequency would be "2". The syndication module borrows from Ian Davis's + /// Open Content Syndication (OCS) directory format. It supercedes the RSS 0.91 + /// skipDay and skipHour elements. + /// + /// See http://web.resource.org/rss/1.0/modules/syndication/ + public var syndication: SyndicationNamespace? + + /// iTunes Podcasting Tags are de facto standard for podcast syndication. + /// See https://help.apple.com/itc/podcasts_connect/#/itcb54353390 + public var iTunes: ITunesNamespace? + + +} + +// MARK: - Equatable + +extension RSSFeed: Equatable { + + public static func ==(lhs: RSSFeed, rhs: RSSFeed) -> Bool { + return + lhs.categories == rhs.categories && + lhs.cloud == rhs.cloud && + lhs.copyright == rhs.copyright && + lhs.description == rhs.description && + lhs.docs == rhs.docs && + lhs.dublinCore == rhs.dublinCore && + lhs.generator == rhs.generator && + lhs.items == rhs.items && + lhs.iTunes == rhs.iTunes && + lhs.language == rhs.language && + lhs.lastBuildDate == rhs.lastBuildDate && + lhs.link == rhs.link && + lhs.managingEditor == rhs.managingEditor && + lhs.pubDate == rhs.pubDate && + lhs.rating == rhs.rating && + lhs.skipDays == rhs.skipDays && + lhs.skipHours == rhs.skipHours && + lhs.syndication == rhs.syndication && + lhs.textInput == rhs.textInput && + lhs.title == rhs.title && + lhs.ttl == rhs.ttl && + lhs.webMaster == rhs.webMaster + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedCategory.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedCategory.swift new file mode 100755 index 0000000..960a179 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedCategory.swift @@ -0,0 +1,93 @@ +// +// RSSFeedCategory.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The category of `<channel>`. Identifies a category or tag to which the feed +/// belongs. +public class RSSFeedCategory { + + /// The element's attributes. + public class Attributes { + + /// A string that identifies a categorization taxonomy. It's an optional + /// attribute of `<category>`. e.g. "http://www.fool.com/cusips" + public var domain: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension RSSFeedCategory { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = RSSFeedCategory.Attributes(attributes: attributeDict) + } + +} + +extension RSSFeedCategory.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.domain = attributeDict["domain"] + + } + +} + +// MARK: - Equatable + +extension RSSFeedCategory: Equatable { + + public static func ==(lhs: RSSFeedCategory, rhs: RSSFeedCategory) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension RSSFeedCategory.Attributes: Equatable { + + public static func ==(lhs: RSSFeedCategory.Attributes, rhs: RSSFeedCategory.Attributes) -> Bool { + return lhs.domain == rhs.domain + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedCloud.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedCloud.swift new file mode 100755 index 0000000..1cb6046 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedCloud.swift @@ -0,0 +1,132 @@ +// +// RSSFeedCloud.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Allows processes to register with a cloud to be notified of updates to +/// the channel, implementing a lightweight publish-subscribe protocol for +/// RSS feeds. +/// +/// Example: <cloud domain="rpc.sys.com" port="80" path="/RPC2" registerProcedure="pingMe" protocol="soap"/> +/// +/// <cloud> is an optional sub-element of <channel>. +/// +/// It specifies a web service that supports the rssCloud interface which can +/// be implemented in HTTP-POST, XML-RPC or SOAP 1.1. +/// +/// Its purpose is to allow processes to register with a cloud to be notified +/// of updates to the channel, implementing a lightweight publish-subscribe +/// protocol for RSS feeds. +/// +/// <cloud domain="rpc.sys.com" port="80" path="/RPC2" registerProcedure="myCloud.rssPleaseNotify" protocol="xml-rpc" /> +/// +/// In this example, to request notification on the channel it appears in, +/// you would send an XML-RPC message to rpc.sys.com on port 80, with a path +/// of /RPC2. The procedure to call is myCloud.rssPleaseNotify. +/// +/// A full explanation of this element and the rssCloud interface is here: +/// http://cyber.law.harvard.edu/rss/soapMeetsRss.html#rsscloudInterface +public class RSSFeedCloud { + + /// The attributes of the `<channel>`'s `<cloud>` element. + public class Attributes { + + /// The domain to register notification to. + public var domain: String? + + /// The port to connect to. + public var port: Int? + + /// The path to the RPC service. e.g. "/RPC2". + public var path: String? + + /// The procedure to call. e.g. "myCloud.rssPleaseNotify" . + public var registerProcedure: String? + + /// The `protocol` specification. Can be HTTP-POST, XML-RPC or SOAP 1.1 - + /// Note: "protocol" is a reserved keyword, so `protocolSpecification` + /// is used instead and refers to the `protocol` attribute of the `cloud` + /// element. + public var protocolSpecification: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + +} + +// MARK: - Initializers + +extension RSSFeedCloud { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = RSSFeedCloud.Attributes(attributes: attributeDict) + } + +} + +extension RSSFeedCloud.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.domain = attributeDict["domain"] + self.port = Int(attributeDict["port"] ?? "") + self.path = attributeDict["path"] + self.registerProcedure = attributeDict["registerProcedure"] + self.protocolSpecification = attributeDict["protocol"] + + } + +} + +// MARK: - Equatable + +extension RSSFeedCloud: Equatable { + + public static func ==(lhs: RSSFeedCloud, rhs: RSSFeedCloud) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension RSSFeedCloud.Attributes: Equatable { + + public static func ==(lhs: RSSFeedCloud.Attributes, rhs: RSSFeedCloud.Attributes) -> Bool { + return + lhs.domain == rhs.domain && + lhs.port == rhs.port && + lhs.path == rhs.path && + lhs.registerProcedure == rhs.registerProcedure && + lhs.protocolSpecification == rhs.protocolSpecification + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedImage.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedImage.swift new file mode 100755 index 0000000..50cfe91 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedImage.swift @@ -0,0 +1,91 @@ +// +// RSSFeedImage.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Specifies a GIF, JPEG or PNG image that can be displayed with the channel. +/// +/// <image> is an optional sub-element of <channel>, which contains three +/// required and three optional sub-elements. +/// +/// <url> is the URL of a GIF, JPEG or PNG image that represents the channel. +/// +/// <title> describes the image, it's used in the ALT attribute of the HTML +/// <img> tag when the channel is rendered in HTML. +/// +/// <link> is the URL of the site, when the channel is rendered, the image +/// is a link to the site. (Note, in practice the image <title> and <link> +/// should have the same value as the channel's <title> and <link>. +/// +/// Optional elements include <width> and <height>, numbers, indicating the +/// width and height of the image in pixels. <description> contains text +/// that is included in the TITLE attribute of the link formed around the +/// image in the HTML rendering. +/// +/// Maximum value for width is 144, default value is 88. +/// +/// Maximum value for height is 400, default value is 31. +public class RSSFeedImage { + + /// The URL of a GIF, JPEG or PNG image that represents the channel. + public var url: String? + + /// Describes the image, it's used in the ALT attribute of the HTML `<img>` + /// tag when the channel is rendered in HTML. + public var title: String? + + /// The URL of the site, when the channel is rendered, the image is a link + /// to the site. (Note, in practice the image `<title>` and `<link>` should + /// have the same value as the channel's `<title>` and `<link>`. + public var link: String? + + /// Optional element `<width>` indicating the width of the image in pixels. + /// Maximum value for width is 144, default value is 88. + public var width: Int? + + /// Optional element `<height>` indicating the height of the image in pixels. + /// Maximum value for height is 400, default value is 31. + public var height: Int? + + /// Contains text that is included in the TITLE attribute of the link formed + /// around the image in the HTML rendering. + public var description: String? + +} + +// MARK: - Equatable + +extension RSSFeedImage: Equatable { + + public static func ==(lhs: RSSFeedImage, rhs: RSSFeedImage) -> Bool { + return + lhs.url == rhs.url && + lhs.title == rhs.title && + lhs.link == rhs.link && + lhs.width == rhs.width && + lhs.height == rhs.height && + lhs.description == rhs.description + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItem.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItem.swift new file mode 100755 index 0000000..8fa1096 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItem.swift @@ -0,0 +1,218 @@ +// +// RSSFeedItem.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// A channel may contain any number of <item>s. An item may represent a +/// "story" -- much like a story in a newspaper or magazine; if so its +/// description is a synopsis of the story, and the link points to the full +/// story. An item may also be complete in itself, if so, the description +/// contains the text (entity-encoded HTML is allowed; see examples: +/// http://cyber.law.harvard.edu/rss/encodingDescriptions.html), and +/// the link and title may be omitted. All elements of an item are optional, +/// however at least one of title or description must be present. +public class RSSFeedItem { + + /// The title of the item. + /// + /// Example: Venice Film Festival Tries to Quit Sinking + public var title: String? + + /// The URL of the item. + /// + /// Example: http://nytimes.com/2004/12/07FEST.html + public var link: String? + + /// The item synopsis. + /// + /// Example: Some of the most heated chatter at the Venice Film Festival this + /// week was about the way that the arrival of the stars at the Palazzo del + /// Cinema was being staged. + public var description: String? + + /// Email address of the author of the item. + /// + /// Example: oprah\@oxygen.net + /// + /// <author> is an optional sub-element of <item>. + /// + /// It's the email address of the author of the item. For newspapers and + /// magazines syndicating via RSS, the author is the person who wrote the + /// article that the <item> describes. For collaborative weblogs, the author + /// of the item might be different from the managing editor or webmaster. + /// For a weblog authored by a single individual it would make sense to omit + /// the <author> element. + /// + /// <author>lawyer@boyer.net (Lawyer Boyer)</author> + public var author: String? + + /// Includes the item in one or more categories. + /// + /// <category> is an optional sub-element of <item>. + /// + /// It has one optional attribute, domain, a string that identifies a + /// categorization taxonomy. + /// + /// The value of the element is a forward-slash-separated string that + /// identifies a hierarchic location in the indicated taxonomy. Processors + /// may establish conventions for the interpretation of categories. + /// + /// Two examples are provided below: + /// + /// <category>Grateful Dead</category> + /// <category domain="http://www.fool.com/cusips">MSFT</category> + /// + /// You may include as many category elements as you need to, for different + /// domains, and to have an item cross-referenced in different parts of the + /// same domain. + public var categories: [RSSFeedItemCategory]? + + /// URL of a page for comments relating to the item. + /// + /// Example: http://www.myblog.org/cgi-local/mt/mt-comments.cgi?entry_id=290 + /// + /// <comments> is an optional sub-element of <item>. + /// + /// If present, it is the url of the comments page for the item. + /// + /// <comments>http://ekzemplo.com/entry/4403/comments</comments> + /// + /// More about comments here: + /// http://cyber.law.harvard.edu/rss/weblogComments.html + public var comments: String? + + /// Describes a media object that is attached to the item. + /// + /// <enclosure> is an optional sub-element of <item>. + /// + /// It has three required attributes. url says where the enclosure is located, + /// length says how big it is in bytes, and type says what its type is, a + /// standard MIME type. + /// + /// The url must be an http url. + /// + /// <enclosure url="http://www.scripting.com/mp3s/weatherReportSuite.mp3" + /// length="12216320" type="audio/mpeg" /> + public var enclosure: RSSFeedItemEnclosure? + + /// A string that uniquely identifies the item. + /// + /// Example: http://inessential.com/2002/09/01.php#a2 + /// + /// <guid> is an optional sub-element of <item>. + /// + /// guid stands for globally unique identifier. It's a string that uniquely + /// identifies the item. When present, an aggregator may choose to use this + /// string to determine if an item is new. + /// + /// <guid>http://some.server.com/weblogItem3207</guid> + /// + /// There are no rules for the syntax of a guid. Aggregators must view them + /// as a string. It's up to the source of the feed to establish the + /// uniqueness of the string. + /// + /// If the guid element has an attribute named "isPermaLink" with a value of + /// true, the reader may assume that it is a permalink to the item, that is, + /// a url that can be opened in a Web browser, that points to the full item + /// described by the <item> element. An example: + /// + /// <guid isPermaLink="true">http://inessential.com/2002/09/01.php#a2</guid> + /// + /// isPermaLink is optional, its default value is true. If its value is false, + /// the guid may not be assumed to be a url, or a url to anything in + /// particular. + public var guid: RSSFeedItemGUID? + + /// Indicates when the item was published. + /// + /// Example: Sun, 19 May 2002 15:21:36 GMT + /// + /// <pubDate> is an optional sub-element of <item>. + /// + /// Its value is a date, indicating when the item was published. If it's a + /// date in the future, aggregators may choose to not display the item until + /// that date. + public var pubDate: Date? + + /// The RSS channel that the item came from. + /// + /// <source> is an optional sub-element of <item>. + /// + /// Its value is the name of the RSS channel that the item came from, derived + /// from its <title>. It has one required attribute, url, which links to the + /// XMLization of the source. + /// + /// <source url="http://www.tomalak.org/links2.xml">Tomalak's Realm</source> + /// + /// The purpose of this element is to propagate credit for links, to + /// publicize the sources of news items. It can be used in the Post command + /// of an aggregator. It should be generated automatically when forwarding + /// an item from an aggregator to a weblog authoring tool. + public var source: RSSFeedItemSource? + + + // MARK: - Namespaces + + /// The Dublin Core Metadata Element Set is a standard for cross-domain + /// resource description. + /// + /// See https://tools.ietf.org/html/rfc5013 + public var dublinCore: DublinCoreNamespace? + + /// A module for the actual content of websites, in multiple formats. + /// + /// See http://web.resource.org/rss/1.0/modules/content/ + public var content: ContentNamespace? + + /// iTunes Podcasting Tags are de facto standard for podcast syndication. + /// see https://help.apple.com/itc/podcasts_connect/#/itcb54353390 + public var iTunes: ITunesNamespace? + + /// Media RSS is a new RSS module that supplements the <enclosure> + /// capabilities of RSS 2.0. + public var media: MediaNamespace? +} + +// MARK: - Equatable + +extension RSSFeedItem: Equatable { + + public static func ==(lhs: RSSFeedItem, rhs: RSSFeedItem) -> Bool { + return + lhs.author == rhs.author && + lhs.categories == rhs.categories && + lhs.comments == rhs.comments && + lhs.content == rhs.content && + lhs.description == rhs.description && + lhs.dublinCore == rhs.dublinCore && + lhs.enclosure == rhs.enclosure && + lhs.guid == rhs.guid && + lhs.iTunes == rhs.iTunes && + lhs.media == rhs.media && + lhs.pubDate == rhs.pubDate && + lhs.source == rhs.source && + lhs.title == rhs.title + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemCategory.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemCategory.swift new file mode 100755 index 0000000..114625d --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemCategory.swift @@ -0,0 +1,111 @@ +// +// RSSFeedItemCategory.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Includes the item in one or more categories. +/// +/// <category> is an optional sub-element of <item>. +/// +/// It has one optional attribute, domain, a string that identifies a +/// categorization taxonomy. +/// +/// The value of the element is a forward-slash-separated string that +/// identifies a hierarchic location in the indicated taxonomy. Processors +/// may establish conventions for the interpretation of categories. +/// +/// Two examples are provided below: +/// +/// <category>Grateful Dead</category> +/// <category domain="http://www.fool.com/cusips">MSFT</category> +/// +/// You may include as many category elements as you need to, for different +/// domains, and to have an item cross-referenced in different parts of the +/// same domain. +public class RSSFeedItemCategory { + + /// The element's attributes. + public class Attributes { + + /// A string that identifies a categorization taxonomy. It's an optional + /// attribute of `<category>`. + /// + /// Example: http://www.fool.com/cusips + public var domain: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension RSSFeedItemCategory { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = RSSFeedItemCategory.Attributes(attributes: attributeDict) + } + +} + +extension RSSFeedItemCategory.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.domain = attributeDict["domain"] + + } + +} + +// MARK: - Equatable + +extension RSSFeedItemCategory: Equatable { + + public static func ==(lhs: RSSFeedItemCategory, rhs: RSSFeedItemCategory) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension RSSFeedItemCategory.Attributes: Equatable { + + public static func ==(lhs: RSSFeedItemCategory.Attributes, rhs: RSSFeedItemCategory.Attributes) -> Bool { + return lhs.domain == rhs.domain + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemEnclosure.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemEnclosure.swift new file mode 100755 index 0000000..8125f1e --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemEnclosure.swift @@ -0,0 +1,114 @@ +// +// RSSFeedItemEnclosure.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Describes a media object that is attached to the item. +/// +/// <enclosure> is an optional sub-element of <item>. +/// +/// It has three required attributes. url says where the enclosure is located, +/// length says how big it is in bytes, and type says what its type is, a +/// standard MIME type. +/// +/// The url must be an http url. +/// +/// <enclosure url="http://www.scripting.com/mp3s/weatherReportSuite.mp3" +/// length="12216320" type="audio/mpeg" /> +public class RSSFeedItemEnclosure { + + /// The element's attributes. + public class Attributes { + + /// Where the enclosure is located. + /// + /// Example: http://www.scripting.com/mp3s/weatherReportSuite.mp3 + public var url: String? + + /// How big the media object is in bytes. + /// + /// Example: 12216320 + public var length: Int64? + + /// Standard MIME type. + /// + /// Example: audio/mpeg + public var type: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + +} + +// MARK: - Initializers + +extension RSSFeedItemEnclosure { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = RSSFeedItemEnclosure.Attributes(attributes: attributeDict) + } + +} + +extension RSSFeedItemEnclosure.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.url = attributeDict["url"] + self.type = attributeDict["type"] + self.length = Int64(attributeDict["length"] ?? "") + + } + +} + +// MARK: - Equatable + +extension RSSFeedItemEnclosure: Equatable { + + public static func ==(lhs: RSSFeedItemEnclosure, rhs: RSSFeedItemEnclosure) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension RSSFeedItemEnclosure.Attributes: Equatable { + + public static func ==(lhs: RSSFeedItemEnclosure.Attributes, rhs: RSSFeedItemEnclosure.Attributes) -> Bool { + return + lhs.url == rhs.url && + lhs.type == rhs.type && + lhs.length == rhs.length + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemGUID.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemGUID.swift new file mode 100755 index 0000000..2a64aa5 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemGUID.swift @@ -0,0 +1,125 @@ +// +// RSSFeedItemGUID.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// A string that uniquely identifies the item. +/// +/// Example: http://inessential.com/2002/09/01.php#a2 +/// +/// <guid> is an optional sub-element of <item>. +/// +/// guid stands for globally unique identifier. It's a string that uniquely +/// identifies the item. When present, an aggregator may choose to use this +/// string to determine if an item is new. +/// +/// <guid>http://some.server.com/weblogItem3207</guid> +/// +/// There are no rules for the syntax of a guid. Aggregators must view them +/// as a string. It's up to the source of the feed to establish the +/// uniqueness of the string. +/// +/// If the guid element has an attribute named "isPermaLink" with a value of +/// true, the reader may assume that it is a permalink to the item, that is, +/// a url that can be opened in a Web browser, that points to the full item +/// described by the <item> element. An example: +/// +/// <guid isPermaLink="true">http://inessential.com/2002/09/01.php#a2</guid> +/// +/// isPermaLink is optional, its default value is true. If its value is false, +/// the guid may not be assumed to be a url, or a url to anything in +/// particular. +public class RSSFeedItemGUID { + + /// The element's attributes. + public class Attributes { + + /// If the guid element has an attribute named "isPermaLink" with a value of + /// true, the reader may assume that it is a permalink to the item, that is, + /// a url that can be opened in a Web browser, that points to the full item + /// described by the <item> element. An example: + /// + /// <guid isPermaLink="true">http://inessential.com/2002/09/01.php#a2</guid> + /// + /// isPermaLink is optional, its default value is true. If its value is false, + /// the guid may not be assumed to be a url, or a url to anything in + /// particular. + public var isPermaLink: Bool? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension RSSFeedItemGUID { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = RSSFeedItemGUID.Attributes(attributes: attributeDict) + } + +} + +extension RSSFeedItemGUID.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.isPermaLink = attributeDict["isPermaLink"]?.toBool() + + } + +} + +// MARK: - Equatable + +extension RSSFeedItemGUID: Equatable { + + public static func ==(lhs: RSSFeedItemGUID, rhs: RSSFeedItemGUID) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension RSSFeedItemGUID.Attributes: Equatable { + + public static func ==(lhs: RSSFeedItemGUID.Attributes, rhs: RSSFeedItemGUID.Attributes) -> Bool { + return lhs.isPermaLink == rhs.isPermaLink + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemSource.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemSource.swift new file mode 100755 index 0000000..8828f3f --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemSource.swift @@ -0,0 +1,105 @@ +// +// RSSFeedItemSource.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The RSS channel that the item came from. +/// +/// <source> is an optional sub-element of <item>. +/// +/// Its value is the name of the RSS channel that the item came from, derived +/// from its <title>. It has one required attribute, url, which links to the +/// XMLization of the source. +/// +/// <source url="http://www.tomalak.org/links2.xml">Tomalak's Realm</source> +/// +/// The purpose of this element is to propagate credit for links, to +/// publicize the sources of news items. It can be used in the Post command +/// of an aggregator. It should be generated automatically when forwarding +/// an item from an aggregator to a weblog authoring tool. +public class RSSFeedItemSource { + + /// The element's attributes. + public class Attributes { + + /// Required attribute of the `Source` element, which links to the + /// XMLization of the source. e.g. "http://www.tomalak.org/links2.xml" + public var url: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + +} + +// MARK: - Initializers + +extension RSSFeedItemSource { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = RSSFeedItemSource.Attributes(attributes: attributeDict) + } + +} + +extension RSSFeedItemSource.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.url = attributeDict["url"] + + } + +} + +// MARK: - Equatable + +extension RSSFeedItemSource: Equatable { + + public static func ==(lhs: RSSFeedItemSource, rhs: RSSFeedItemSource) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension RSSFeedItemSource.Attributes: Equatable { + + public static func ==(lhs: RSSFeedItemSource.Attributes, rhs: RSSFeedItemSource.Attributes) -> Bool { + return lhs.url == rhs.url + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedSkipDay.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedSkipDay.swift new file mode 100755 index 0000000..ddf2b9d --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedSkipDay.swift @@ -0,0 +1,70 @@ +// +// RSSFeedSkipDay.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// A hint for aggregators telling them which days they can skip. +/// +/// An XML element that contains up to seven <day> sub-elements whose value +/// is Monday, Tuesday, Wednesday, Thursday, Friday, Saturday or Sunday. +/// Aggregators may not read the channel during days listed in the skipDays +/// element. +/// +/// - monday: Aggregator hint to skip parsing on `Monday`. +/// - tuesday: Aggregator hint to skip parsing on `Tuesday`. +/// - wednesday: Aggregator hint to skip parsing on `Wednesday`. +/// - thursday: Aggregator hint to skip parsing on `Thursday`. +/// - friday: Aggregator hint to skip parsing on `Friday`. +/// - saturday: Aggregator hint to skip parsing on `Saturday`. +/// - sunday: Aggregator hint to skip parsing on `Sunday`. +public enum RSSFeedSkipDay: String { + case monday = "monday" + case tuesday = "tuesday" + case wednesday = "wednesday" + case thursday = "thursday" + case friday = "friday" + case saturday = "saturday" + case sunday = "sunday" +} + +extension RSSFeedSkipDay { + + /// Lowercase the incoming `rawValue` string to try and match the + /// `RSSFeedSkipDay`'s `rawValue` + /// + /// - Parameter rawValue: The raw value + public init?(rawValue: String) { + switch rawValue.lowercased() { + case "monday": self = .monday + case "tuesday": self = .tuesday + case "wednesday": self = .wednesday + case "thursday": self = .thursday + case "friday": self = .friday + case "saturday": self = .saturday + case "sunday": self = .sunday + default: return nil + } + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedSkipHour.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedSkipHour.swift new file mode 100755 index 0000000..3e41d45 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedSkipHour.swift @@ -0,0 +1,35 @@ +// +// RSSFeedSkipHour.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// A hint for aggregators telling them which hours they can skip. +/// +/// An XML element that contains up to 24 <hour> sub-elements whose value is a +/// number between 0 and 23, representing a time in GMT, when aggregators, if they +/// support the feature, may not read the channel on hours listed in the skipHours +/// element. +/// +/// The hour beginning at midnight is hour zero. +public typealias RSSFeedSkipHour = Int diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedTextInput.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedTextInput.swift new file mode 100755 index 0000000..924b79e --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedTextInput.swift @@ -0,0 +1,71 @@ +// +// RSSFeedTextInput.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Specifies a text input box that can be displayed with the channel. +/// +/// A channel may optionally contain a <textInput> sub-element, which contains +/// four required sub-elements. +/// +/// <title> -- The label of the Submit button in the text input area. +/// +/// <description> -- Explains the text input area. +/// +/// <name> -- The name of the text object in the text input area. +/// +/// <link> -- The URL of the CGI script that processes text input requests. +/// +/// The purpose of the <textInput> element is something of a mystery. You can +/// use it to specify a search engine box. Or to allow a reader to provide +/// feedback. Most aggregators ignore it. +public class RSSFeedTextInput { + + /// The label of the Submit button in the text input area. + public var title: String? + + /// Explains the text input area. + public var description: String? + + /// The name of the text object in the text input area. + public var name: String? + + /// The URL of the CGI script that processes text input requests. + public var link: String? + +} + +// MARK: - Equatable + +extension RSSFeedTextInput: Equatable { + + public static func ==(lhs: RSSFeedTextInput, rhs: RSSFeedTextInput) -> Bool { + return + lhs.title == rhs.title && + lhs.description == rhs.description && + lhs.name == rhs.name && + lhs.link == lhs.link + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSPath.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSPath.swift new file mode 100755 index 0000000..a674587 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSPath.swift @@ -0,0 +1,184 @@ +// +// RSSPath.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Describes the individual path for each XML DOM element of an RSS feed +/// +/// See http://web.resource.org/rss/1.0/modules/content/ +enum RSSPath: String { + + case rss = "/rss" + case rssChannel = "/rss/channel" + case rssChannelTitle = "/rss/channel/title" + case rssChannelLink = "/rss/channel/link" + case rssChannelDescription = "/rss/channel/description" + case rssChannelLanguage = "/rss/channel/language" + case rssChannelCopyright = "/rss/channel/copyright" + case rssChannelManagingEditor = "/rss/channel/managingEditor" + case rssChannelWebMaster = "/rss/channel/webMaster" + case rssChannelPubDate = "/rss/channel/pubDate" + case rssChannelLastBuildDate = "/rss/channel/lastBuildDate" + case rssChannelCategory = "/rss/channel/category" + case rssChannelGenerator = "/rss/channel/generator" + case rssChannelDocs = "/rss/channel/docs" + case rssChannelCloud = "/rss/channel/cloud" + case rssChannelRating = "/rss/channel/rating" + case rssChannelTTL = "/rss/channel/ttl" + case rssChannelImage = "/rss/channel/image" + case rssChannelImageURL = "/rss/channel/image/url" + case rssChannelImageTitle = "/rss/channel/image/title" + case rssChannelImageLink = "/rss/channel/image/link" + case rssChannelImageWidth = "/rss/channel/image/width" + case rssChannelImageHeight = "/rss/channel/image/height" + case rssChannelImageDescription = "/rss/channel/image/description" + case rssChannelTextInput = "/rss/channel/textInput" + case rssChannelTextInputTitle = "/rss/channel/textInput/title" + case rssChannelTextInputDescription = "/rss/channel/textInput/description" + case rssChannelTextInputName = "/rss/channel/textInput/name" + case rssChannelTextInputLink = "/rss/channel/textInput/link" + case rssChannelSkipHours = "/rss/channel/skipHours" + case rssChannelSkipHoursHour = "/rss/channel/skipHours/hour" + case rssChannelSkipDays = "/rss/channel/skipDays" + case rssChannelSkipDaysDay = "/rss/channel/skipDays/day" + case rssChannelItem = "/rss/channel/item" + case rssChannelItemTitle = "/rss/channel/item/title" + case rssChannelItemLink = "/rss/channel/item/link" + case rssChannelItemDescription = "/rss/channel/item/description" + case rssChannelItemAuthor = "/rss/channel/item/author" + case rssChannelItemCategory = "/rss/channel/item/category" + case rssChannelItemComments = "/rss/channel/item/comments" + case rssChannelItemEnclosure = "/rss/channel/item/enclosure" + case rssChannelItemGUID = "/rss/channel/item/guid" + case rssChannelItemPubDate = "/rss/channel/item/pubDate" + case rssChannelItemSource = "/rss/channel/item/source" + + // Content + + case rssChannelItemContentEncoded = "/rss/channel/item/content:encoded" + + // Syndication + + case rssChannelSyndicationUpdatePeriod = "/rss/channel/sy:updatePeriod" + case rssChannelSyndicationUpdateFrequency = "/rss/channel/sy:updateFrequency" + case rssChannelSyndicationUpdateBase = "/rss/channel/sy:updateBase" + + // Dublin Core + + case rssChannelDublinCoreTitle = "/rss/channel/dc:title" + case rssChannelDublinCoreCreator = "/rss/channel/dc:creator" + case rssChannelDublinCoreSubject = "/rss/channel/dc:subject" + case rssChannelDublinCoreDescription = "/rss/channel/dc:description" + case rssChannelDublinCorePublisher = "/rss/channel/dc:publisher" + case rssChannelDublinCoreContributor = "/rss/channel/dc:contributor" + case rssChannelDublinCoreDate = "/rss/channel/dc:date" + case rssChannelDublinCoreType = "/rss/channel/dc:type" + case rssChannelDublinCoreFormat = "/rss/channel/dc:format" + case rssChannelDublinCoreIdentifier = "/rss/channel/dc:identifier" + case rssChannelDublinCoreSource = "/rss/channel/dc:source" + case rssChannelDublinCoreLanguage = "/rss/channel/dc:language" + case rssChannelDublinCoreRelation = "/rss/channel/dc:relation" + case rssChannelDublinCoreCoverage = "/rss/channel/dc:coverage" + case rssChannelDublinCoreRights = "/rss/channel/dc:rights" + case rssChannelItemDublinCoreTitle = "/rss/channel/item/dc:title" + case rssChannelItemDublinCoreCreator = "/rss/channel/item/dc:creator" + case rssChannelItemDublinCoreSubject = "/rss/channel/item/dc:subject" + case rssChannelItemDublinCoreDescription = "/rss/channel/item/dc:description" + case rssChannelItemDublinCorePublisher = "/rss/channel/item/dc:publisher" + case rssChannelItemDublinCoreContributor = "/rss/channel/item/dc:contributor" + case rssChannelItemDublinCoreDate = "/rss/channel/item/dc:date" + case rssChannelItemDublinCoreType = "/rss/channel/item/dc:type" + case rssChannelItemDublinCoreFormat = "/rss/channel/item/dc:format" + case rssChannelItemDublinCoreIdentifier = "/rss/channel/item/dc:identifier" + case rssChannelItemDublinCoreSource = "/rss/channel/item/dc:source" + case rssChannelItemDublinCoreLanguage = "/rss/channel/item/dc:language" + case rssChannelItemDublinCoreRelation = "/rss/channel/item/dc:relation" + case rssChannelItemDublinCoreCoverage = "/rss/channel/item/dc:coverage" + case rssChannelItemDublinCoreRights = "/rss/channel/item/dc:rights" + + // iTunes Podcasting Tags + + case rssChannelItunesAuthor = "/rss/channel/itunes:author" + case rssChannelItunesBlock = "/rss/channel/itunes:block" + case rssChannelItunesCategory = "/rss/channel/itunes:category" + case rssChannelItunesSubcategory = "/rss/channel/itunes:category/itunes:category" + case rssChannelItunesImage = "/rss/channel/itunes:image" + case rssChannelItunesExplicit = "/rss/channel/itunes:explicit" + case rssChannelItunesComplete = "/rss/channel/itunes:complete" + case rssChannelItunesNewFeedURL = "/rss/channel/itunes:new-feed-url" + case rssChannelItunesOwner = "/rss/channel/itunes:owner" + case rssChannelItunesOwnerEmail = "/rss/channel/itunes:owner/itunes:email" + case rssChannelItunesOwnerName = "/rss/channel/itunes:owner/itunes:name" + case rssChannelItunesSubtitle = "/rss/channel/itunes:subtitle" + case rssChannelItunesSummary = "/rss/channel/itunes:summary" + case rssChannelItunesKeywords = "/rss/channel/itunes:keywords" + + case rssChannelItemItunesAuthor = "/rss/channel/item/itunes:author" + case rssChannelItemItunesBlock = "/rss/channel/item/itunes:block" + case rssChannelItemItunesImage = "/rss/channel/item/itunes:image" + case rssChannelItemItunesDuration = "/rss/channel/item/itunes:duration" + case rssChannelItemItunesExplicit = "/rss/channel/item/itunes:explicit" + case rssChannelItemItunesIsClosedCaptioned = "/rss/channel/item/itunes:isClosedCaptioned" + case rssChannelItemItunesOrder = "/rss/channel/item/itunes:order" + case rssChannelItemItunesSubtitle = "/rss/channel/item/itunes:subtitle" + case rssChannelItemItunesSummary = "/rss/channel/item/itunes:summary" + case rssChannelItemItunesKeywords = "/rss/channel/item/itunes:keywords" + + // MARK: Media + + case rssChannelItemMediaThumbnail = "/rss/channel/item/media:thumbnail" + case rssChannelItemMediaContent = "/rss/channel/item/media:content" + case rssChannelItemMediaCommunity = "/rss/channel/item/media:community" + case rssChannelItemMediaCommunityMediaStarRating = "/rss/channel/item/media:community/media:starRating" + case rssChannelItemMediaCommunityMediaStatistics = "/rss/channel/item/media:community/media:statistics" + case rssChannelItemMediaCommunityMediaTags = "/rss/channel/item/media:community/media:tags" + case rssChannelItemMediaComments = "/rss/channel/item/media:comments" + case rssChannelItemMediaCommentsMediaComment = "/rss/channel/item/media:comments/media:comment" + case rssChannelItemMediaEmbed = "/rss/channel/item/media:embed" + case rssChannelItemMediaEmbedMediaParam = "/rss/channel/item/media:embed/media:param" + case rssChannelItemMediaResponses = "/rss/channel/item/media:responses" + case rssChannelItemMediaResponsesMediaResponse = "/rss/channel/item/media:responses/media:response" + case rssChannelItemMediaBackLinks = "/rss/channel/item/media:backLinks" + case rssChannelItemMediaBackLinksBackLink = "/rss/channel/item/media:backLinks/media:backLink" + case rssChannelItemMediaStatus = "/rss/channel/item/media:status" + case rssChannelItemMediaPrice = "/rss/channel/item/media:price" + case rssChannelItemMediaLicense = "/rss/channel/item/media:license" + case rssChannelItemMediaSubTitle = "/rss/channel/item/media:subTitle" + case rssChannelItemMediaPeerLink = "/rss/channel/item/media:peerLink" + case rssChannelItemMediaLocation = "/rss/channel/item/media:location" + case rssChannelItemMediaLocationPosition = "/rss/channel/item/media:location/georss:where/gml:Point/gml:pos" + case rssChannelItemMediaRestriction = "/rss/channel/item/media:restriction" + case rssChannelItemMediaScenes = "/rss/channel/item/media:scenes" + case rssChannelItemMediaScenesMediaScene = "/rss/channel/item/media:scenes/media:scene" + case rssChannelItemMediaScenesMediaSceneSceneTitle = "/rss/channel/item/media:scenes/media:scene/sceneTitle" + case rssChannelItemMediaScenesMediaSceneSceneDescription = "/rss/channel/item/media:scenes/media:scene/sceneDescription" + case rssChannelItemMediaScenesMediaSceneSceneStartTime = "/rss/channel/item/media:scenes/media:scene/sceneStartTime" + case rssChannelItemMediaScenesMediaSceneSceneEndTime = "/rss/channel/item/media:scenes/media:scene/sceneEndTime" + case rssChannelItemMediaGroup = "/rss/channel/item/media:group" + case rssChannelItemMediaGroupMediaCredit = "/rss/channel/item/media:group/media:credit" + case rssChannelItemMediaGroupMediaCategory = "/rss/channel/item/media:group/media:category" + case rssChannelItemMediaGroupMediaRating = "/rss/channel/item/media:group/media:rating" + case rssChannelItemMediaGroupMediaContent = "/rss/channel/item/media:group/media:content" + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Parser/FeedDataType.swift b/Pods/FeedKit/Sources/FeedKit/Parser/FeedDataType.swift new file mode 100644 index 0000000..b86a94f --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Parser/FeedDataType.swift @@ -0,0 +1,51 @@ +// +// FeedDataType.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Types of data to determine how to parse a feed. +/// +/// - xml: XML Data Type. +/// - json: JSON Data Type. +enum FeedDataType: String { + case xml + case json +} + +extension FeedDataType { + + /// A `FeedDataType` from the specified `Data` object + /// + /// - Parameter data: The `Data` object. + init?(data: Data) { + guard let string = String(data: data, encoding: .utf8) else { return nil } + guard let char = string.trimmingCharacters(in: .whitespacesAndNewlines).first else { return nil } + switch char { + case "<": self = .xml + case "{": self = .json + default: return nil + } + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Parser/FeedParser.swift b/Pods/FeedKit/Sources/FeedKit/Parser/FeedParser.swift new file mode 100644 index 0000000..11203c8 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Parser/FeedParser.swift @@ -0,0 +1,93 @@ +// +// FeedParser.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +import Dispatch + +/// An RSS and Atom feed parser. `FeedParser` uses `Foundation`'s `XMLParser`. +public class FeedParser { + + /// A FeedParser handler provider. + let parser: FeedParserProtocol + + /// Initializes the parser with the xml or json contents encapsulated in a + /// given data object. + /// + /// - Parameter data: An instance of `FeedParser`. + public init?(data: Data) { + guard let decoded = data.toUtf8() else { return nil } + + guard let feedDataType = FeedDataType(data: decoded) else { return nil } + switch feedDataType { + case .json: self.parser = JSONFeedParser(data: decoded) + case .xml: self.parser = XMLFeedParser(data: decoded) + } + } + + /// Initializes the parser with the XML content referenced by the given URL. + /// + /// - Parameter URL: An instance of `FeedParser`. + public convenience init?(URL: URL) { + guard let data = try? Data(contentsOf: URL) else { + return nil + } + self.init(data: data) + } + + /// Starts parsing the feed. + /// + /// - Returns: The parsed `Result`. + public func parse() -> Result { + return self.parser.parse() + } + + /// Starts parsing the feed asynchronously. Parsing runs by default on the + /// global queue. You are responsible to manually bring the result closure + /// to whichever queue is apropriate, if any. + /// + /// Usually to the Main queue if UI Updates are needed. + /// + /// DispatchQueue.main.async { + /// // UI Updates + /// } + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. + /// - result: The parsed `Result`. + public func parseAsync( + queue: DispatchQueue = DispatchQueue.global(), + result: @escaping (Result) -> Void) + { + queue.async { + result(self.parse()) + } + } + + /// Stops parsing XML feeds. + public func abortParsing() { + guard let xmlFeedParser = self.parser as? XMLFeedParser else { return } + xmlFeedParser.xmlParser.abortParsing() + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Parser/FeedParserProtocol.swift b/Pods/FeedKit/Sources/FeedKit/Parser/FeedParserProtocol.swift new file mode 100644 index 0000000..f562da7 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Parser/FeedParserProtocol.swift @@ -0,0 +1,31 @@ +// +// FeedParserProtocol.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The protocol for Parsing handlers. +protocol FeedParserProtocol { + init(data: Data) + func parse() -> Result +} diff --git a/Pods/FeedKit/Sources/FeedKit/Parser/JSONFeedParser.swift b/Pods/FeedKit/Sources/FeedKit/Parser/JSONFeedParser.swift new file mode 100644 index 0000000..2c239c1 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Parser/JSONFeedParser.swift @@ -0,0 +1,61 @@ +// +// JSONFeedParser.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The actual engine behind the `FeedKit` framework. `JSONFeedParser` handles +/// the parsing of JSON Feeds. +/// +/// See: https://jsonfeed.org/version/1 +class JSONFeedParser: FeedParserProtocol { + + let data: Data + + required public init(data: Data) { + self.data = data + } + + func parse() -> Result { + + do { + + let jsonObject = try JSONSerialization.jsonObject(with: data, options: .allowFragments) + + guard let jsonDictionary = jsonObject as? [String: Any?] else { + return Result.failure(ParserError.internalError(reason: "Unable to cast serialized json.").value) + } + + guard let jsonFeed = JSONFeed(dictionary: jsonDictionary) else { + return Result.failure(ParserError.feedNotFound.value) + } + + return Result.json(jsonFeed) + + } catch { + return Result.failure(NSError(domain: error.localizedDescription, code: -1)) + } + + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Parser/ParserError.swift b/Pods/FeedKit/Sources/FeedKit/Parser/ParserError.swift new file mode 100644 index 0000000..7c500db --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Parser/ParserError.swift @@ -0,0 +1,82 @@ +// +// ParserError.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + + +/// Error types with `NSError` codes and user info providers +/// +/// - feedNotFound: Couldn't parse any known feed. +/// - feedCDATABlockEncodingError: Unable to convert the bytes in `CDATABlock` +/// to Unicode characters using the UTF-8 encoding. +/// - internalError: An internal error from which the user cannot recover. +public enum ParserError { + + case feedNotFound + case feedCDATABlockEncodingError(path: String) + case internalError(reason: String) + + /// An error's code for the specified case. + var code: Int { + switch self { + case .feedNotFound: return -1000 + case .feedCDATABlockEncodingError: return -10001 + case .internalError(_): return -90000 + } + } + + /// The error's userInfo dictionary for the specified case. + var userInfo: [String: String] { + switch self { + case .feedNotFound: + return [ + NSLocalizedDescriptionKey: "Feed not found", + NSLocalizedFailureReasonErrorKey: "Couldn't parse any known feed", + NSLocalizedRecoverySuggestionErrorKey: "Provide a valid Atom/RSS/JSON feed " + ] + + case .feedCDATABlockEncodingError(let path): + return [ + NSLocalizedDescriptionKey: "`CDATAblock` encoding error", + NSLocalizedFailureReasonErrorKey: "Unable to convert the bytes in `CDATABlock` to Unicode characters using the UTF-8 encoding at current path: \(path)", + NSLocalizedRecoverySuggestionErrorKey: "Make sure the encoding provided in a `CDATABlock` is encoded as UTF-8" + ] + + case .internalError(let reason): + return [ + NSLocalizedDescriptionKey: "Internal unresolved error: \(reason)", + NSLocalizedFailureReasonErrorKey: "Unable to recover from an internal unresolved error: \(reason)", + NSLocalizedRecoverySuggestionErrorKey: "If you're seeing this error you probably should open an issue on github" + ] + + } + + } + + /// The `NSError` from the specified case. + var value: NSError { + return NSError(domain:"com.feedkit.error", code: self.code, userInfo: self.userInfo) + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Parser/Result.swift b/Pods/FeedKit/Sources/FeedKit/Parser/Result.swift new file mode 100644 index 0000000..ccc90d2 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Parser/Result.swift @@ -0,0 +1,92 @@ +// +// Result.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Used to provide the result of a parsed feed, whether the parsing was +/// successfull or encountered an error. +/// +/// - atom: The parsed `AtomFeed` model. +/// - rss: The parsed `RSSFeed` model. +/// - json: The parsed `JSONFeed` model. +/// - failure: The failure `NSError` generated from parsing errors. +public enum Result { + + case atom(AtomFeed) + case rss(RSSFeed) + case json(JSONFeed) + case failure(NSError) + + /// Returns `true` if the result is a success, `false` otherwise. + public var isSuccess: Bool { + switch self { + case .atom: return true + case .rss: return true + case .json: return true + case .failure: return false + } + } + + /// Returns `true` if the result is a failure, `false` otherwise. + public var isFailure: Bool { + return !isSuccess + } + + /// Returns the parsed rss feed value if the result is a success, `nil` + /// otherwise. + public var rssFeed: RSSFeed? { + switch self { + case .rss(let value): return value + default: return nil + } + } + + /// Returns the parsed atom feed if the result is a success, `nil` + /// otherwise. + public var atomFeed: AtomFeed? { + switch self { + case .atom(let value): return value + default: return nil + } + } + + /// Returns the parsed json feed if the result is a success, `nil` + /// otherwise. + public var jsonFeed: JSONFeed? { + switch self { + case .json(let value): return value + default: return nil + } + } + + /// Returns the associated error value if the result is a failure, `nil` + /// otherwise. + public var error: NSError? { + switch self { + case .failure(let error): return error + default: return nil + } + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Parser/XMLFeedParser.swift b/Pods/FeedKit/Sources/FeedKit/Parser/XMLFeedParser.swift new file mode 100644 index 0000000..e1adbdb --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Parser/XMLFeedParser.swift @@ -0,0 +1,190 @@ +// +// XMLFeedParser.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The actual engine behind the `FeedKit` framework. `XMLFeedParser` handles +/// the parsing of RSS and Atom feeds. It is an `XMLParserDelegate` of +/// itself. +class XMLFeedParser: NSObject, XMLParserDelegate, FeedParserProtocol { + + /// The Feed Type currently being parsed. The Initial value of this variable + /// is unknown until a recognizable element that matches a feed type is + /// found. + var feedType: XMLFeedType? + + /// The RSS feed model. + var rssFeed: RSSFeed? + + /// The Atom feed model. + var atomFeed: AtomFeed? + + /// The XML Parser. + let xmlParser: XMLParser + + /// An XML Feed Parser, for rss and atom feeds. + /// + /// - Parameter data: A `Data` object containing an XML feed. + required init(data: Data) { + self.xmlParser = XMLParser(data: data) + super.init() + self.xmlParser.delegate = self + } + + /// The current path along the XML's DOM elements. Path components are + /// updated to reflect the current XML element being parsed. + /// e.g. "/rss/channel/title" means it's currently parsing the channels + /// `<title>` element. + fileprivate var currentXMLDOMPath: URL = URL(string: "/")! + + /// A parsing error, if any. + var parsingError: NSError? + + /// Starts parsing the feed. + func parse() -> Result { + let _ = self.xmlParser.parse() + + if let error = parsingError { + return Result.failure(error) + } + + guard let feedType = self.feedType else { + return Result.failure(ParserError.feedNotFound.value) + } + + switch feedType { + case .atom: return Result.atom(self.atomFeed!) + case .rdf, .rss: return Result.rss(self.rssFeed!) + } + + } + + /// Redirects characters found between XML elements to their proper model + /// mappers based on the `currentXMLDOMPath`. + /// + /// - Parameter string: The characters to map. + fileprivate func map(_ string: String) { + guard let feedType = self.feedType else { return } + + switch feedType { + case .atom: + if let path = AtomPath(rawValue: self.currentXMLDOMPath.absoluteString) { + self.atomFeed?.map(string, for: path) + } + + case .rdf: + if let path = RDFPath(rawValue: self.currentXMLDOMPath.absoluteString) { + self.rssFeed?.map(string, for: path) + } + + case .rss: + if let path = RSSPath(rawValue: self.currentXMLDOMPath.absoluteString) { + self.rssFeed?.map(string, for: path) + } + + } + + } + +} + +// MARK: - XMLParser delegate + +extension XMLFeedParser { + + func parser( + _ parser: XMLParser, + didStartElement elementName: String, + namespaceURI: String?, + qualifiedName qName: String?, + attributes attributeDict: [String : String]) + { + + // Update the current path along the XML's DOM elements by appending the new component with `elementName`. + self.currentXMLDOMPath = self.currentXMLDOMPath.appendingPathComponent(elementName) + + // Get the feed type from the element, if it hasn't been done yet. + guard let feedType = self.feedType else { + self.feedType = XMLFeedType(rawValue: elementName) + return + } + + switch feedType { + case .atom: + if self.atomFeed == nil { + self.atomFeed = AtomFeed() + } + if let path = AtomPath(rawValue: self.currentXMLDOMPath.absoluteString) { + self.atomFeed?.map(attributeDict, for: path) + } + + case .rdf: + if self.rssFeed == nil { + self.rssFeed = RSSFeed() + } + if let path = RDFPath(rawValue: self.currentXMLDOMPath.absoluteString) { + self.rssFeed?.map(attributeDict, for: path) + } + + case .rss: + if self.rssFeed == nil { + self.rssFeed = RSSFeed() + } + if let path = RSSPath(rawValue: self.currentXMLDOMPath.absoluteString) { + self.rssFeed?.map(attributeDict, for: path) + } + + } + + } + + func parser( + _ parser: XMLParser, + didEndElement elementName: String, + namespaceURI: String?, + qualifiedName qName: String?) + { + // Update the current path along the XML's DOM elements by deleting last component. + self.currentXMLDOMPath = self.currentXMLDOMPath.deletingLastPathComponent() + } + + func parser(_ parser: XMLParser, foundCDATA CDATABlock: Data) + { + guard let string = String(data: CDATABlock, encoding: .utf8) else { + self.xmlParser.abortParsing() + self.parsingError = ParserError.feedCDATABlockEncodingError(path: self.currentXMLDOMPath.absoluteString).value + return + } + self.map(string) + } + + func parser(_ parser: XMLParser, foundCharacters string: String) { + self.map(string) + } + + func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) { + self.parsingError = NSError(domain: parseError.localizedDescription, code: -1) + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Parser/XMLFeedType.swift b/Pods/FeedKit/Sources/FeedKit/Parser/XMLFeedType.swift new file mode 100755 index 0000000..ca3cd0d --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Parser/XMLFeedType.swift @@ -0,0 +1,36 @@ +// +// XMLFeedType.swift +// +// Copyright (c) 2017 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Types of feed. The `rawValue` matches the top-level XML element of a feed. +/// +/// - atom: The `Atom Syndication Format feed type. +/// - rdf: The Really Simple Syndication feed type version 0.90. +/// - rss: The Really Simple Syndication feed type version 2.0. +enum XMLFeedType: String { + case atom = "feed" + case rdf = "rdf:RDF" + case rss = "rss" +} diff --git a/Pods/GRDB.swift/GRDB/Core/Configuration.swift b/Pods/GRDB.swift/GRDB/Core/Configuration.swift new file mode 100644 index 0000000..cc789ad --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Configuration.swift @@ -0,0 +1,80 @@ +import Foundation +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif + +/// Configuration for a DatabaseQueue or DatabasePool. +public struct Configuration { + + // MARK: - Misc options + + /// If true, foreign key constraints are checked. + /// + /// Default: true + public var foreignKeysEnabled: Bool = true + + /// If true, database modifications are disallowed. + /// + /// Default: false + public var readonly: Bool = false + + /// A function that is called on every statement executed by the database. + /// + /// Default: nil + public var trace: TraceFunction? + + + // MARK: - Encryption + + #if SQLITE_HAS_CODEC + /// The passphrase for encrypted database. + /// + /// Default: nil + public var passphrase: String? + #endif + + + // MARK: - Transactions + + /// The default kind of transaction. + /// + /// Default: deferred + public var defaultTransactionKind: Database.TransactionKind = .deferred + + + // MARK: - Concurrency + + /// The behavior in case of SQLITE_BUSY error. See https://www.sqlite.org/rescode.html#busy + /// + /// Default: immediateError + public var busyMode: Database.BusyMode = .immediateError + + /// The maximum number of concurrent readers (applies to database + /// pools only). + /// + /// Default: 5 + public var maximumReaderCount: Int = 5 + + + // MARK: - Factory Configuration + + /// Creates a factory configuration + public init() { } + + + // MARK: - Not Public + + var threadingMode: Database.ThreadingMode = .`default` + var SQLiteConnectionDidOpen: (() -> ())? + var SQLiteConnectionWillClose: ((SQLiteConnection) -> ())? + var SQLiteConnectionDidClose: (() -> ())? + var SQLiteOpenFlags: Int32 { + let readWriteFlags = readonly ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE) + return threadingMode.SQLiteOpenFlags | readWriteFlags + } +} + +/// A tracing function that takes an SQL string. +public typealias TraceFunction = (String) -> Void diff --git a/Pods/GRDB.swift/GRDB/Core/Cursor.swift b/Pods/GRDB.swift/GRDB/Core/Cursor.swift new file mode 100644 index 0000000..525b9c3 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Cursor.swift @@ -0,0 +1,344 @@ +extension Array { + /// Creates an array containing the elements of a cursor. + /// + /// let cursor = try String.fetchCursor(db, "SELECT 'foo' UNION ALL SELECT 'bar'") + /// let strings = try Array(cursor) // ["foo", "bar"] + public init<C: Cursor>(_ cursor: C) throws where C.Element == Element { + self.init() + while let element = try cursor.next() { + append(element) + } + } +} + +extension Set { + /// Creates a set containing the elements of a cursor. + /// + /// let cursor = try String.fetchCursor(db, "SELECT 'foo' UNION ALL SELECT 'foo'") + /// let strings = try Set(cursor) // ["foo"] + public init<C: Cursor>(_ cursor: C) throws where C.Element == Element { + self.init() + while let element = try cursor.next() { + insert(element) + } + } +} + +/// A type that supplies the values of some external resource, one at a time. +/// +/// ## Overview +/// +/// The most common way to iterate over the elements of a cursor is to use a +/// `while` loop: +/// +/// let cursor = ... +/// while let element = try cursor.next() { +/// ... +/// } +/// +/// ## Relationship with standard Sequence and IteratorProtocol +/// +/// Cursors share traits with lazy sequences and iterators from the Swift +/// standard library. Differences are: +/// +/// - Cursor types are classes, and have a lifetime. +/// - Cursor iteration may throw errors. +/// - A cursor can not be repeated. +/// +/// The protocol comes with default implementations for many operations similar +/// to those defined by Swift's LazySequenceProtocol: +/// +/// - `func contains(Self.Element)` +/// - `func contains(where: (Self.Element) throws -> Bool)` +/// - `func enumerated()` +/// - `func filter((Self.Element) throws -> Bool)` +/// - `func first(where: (Self.Element) throws -> Bool)` +/// - `func flatMap<ElementOfResult>((Self.Element) throws -> ElementOfResult?)` +/// - `func flatMap<SegmentOfResult>((Self.Element) throws -> SegmentOfResult)` +/// - `func forEach((Self.Element) throws -> Void)` +/// - `func joined()` +/// - `func map<T>((Self.Element) throws -> T)` +/// - `func reduce<Result>(Result, (Result, Self.Element) throws -> Result)` +public protocol Cursor : class { + /// The type of element traversed by the cursor. + associatedtype Element + + /// Advances to the next element and returns it, or nil if no next element + /// exists. Once nil has been returned, all subsequent calls return nil. + func next() throws -> Element? +} + +/// A type-erased cursor of Element. +/// +/// This cursor forwards its next() method to an arbitrary underlying cursor +/// having the same Element type, hiding the specifics of the underlying +/// cursor. +public class AnyCursor<Element> : Cursor { + /// Creates a cursor that wraps a base cursor but whose type depends only on + /// the base cursor’s element type + public init<C: Cursor>(_ base: C) where C.Element == Element { + element = base.next + } + + /// Creates a cursor that wraps the given closure in its next() method + public init(_ body: @escaping () throws -> Element?) { + element = body + } + + /// Advances to the next element and returns it, or nil if no next + /// element exists. + public func next() throws -> Element? { + return try element() + } + + private let element: () throws -> Element? +} + +extension Cursor { + /// Returns a Boolean value indicating whether the cursor contains an + /// element that satisfies the given predicate. + /// + /// - parameter predicate: A closure that takes an element of the cursor as + /// its argument and returns a Boolean value that indicates whether the + /// passed element represents a match. + /// - returns: true if the cursor contains an element that satisfies + /// predicate; otherwise, false. + public func contains(where predicate: (Element) throws -> Bool) throws -> Bool { + while let element = try next() { + if try predicate(element) { + return true + } + } + return false + } + + /// Returns a cursor of pairs (n, x), where n represents a consecutive + /// integer starting at zero, and x represents an element of the cursor. + /// + /// let cursor = try String.fetchCursor(db, "SELECT 'foo' UNION ALL SELECT 'bar'") + /// let c = cursor.enumerated() + /// while let (n, x) = c.next() { + /// print("\(n): \(x)") + /// } + /// // Prints: "0: foo" + /// // Prints: "1: bar" + public func enumerated() -> EnumeratedCursor<Self> { + return EnumeratedCursor(self) + } + + /// Returns the elements of the cursor that satisfy the given predicate. + public func filter(_ isIncluded: @escaping (Element) throws -> Bool) -> FilterCursor<Self> { + return FilterCursor(self, isIncluded) + } + + /// Returns the first element of the cursor that satisfies the given + /// predicate or nil if no such element is found. + public func first(where predicate: (Element) throws -> Bool) throws -> Element? { + while let element = try next() { + if try predicate(element) { + return element + } + } + return nil + } + + /// Returns a cursor over the concatenated non-nil results of mapping + /// transform over this cursor. + public func flatMap<ElementOfResult>(_ transform: @escaping (Element) throws -> ElementOfResult?) -> MapCursor<FilterCursor<MapCursor<Self, ElementOfResult?>>, ElementOfResult> { + return map(transform).filter { $0 != nil }.map { $0! } + } + + /// Returns a cursor over the concatenated results of mapping transform + /// over self. + public func flatMap<SegmentOfResult: Sequence>(_ transform: @escaping (Element) throws -> SegmentOfResult) -> FlattenCursor<MapCursor<Self, IteratorCursor<SegmentOfResult.Iterator>>> { + return flatMap { try IteratorCursor(transform($0)) } + } + + /// Returns a cursor over the concatenated results of mapping transform + /// over self. + public func flatMap<SegmentOfResult: Cursor>(_ transform: @escaping (Element) throws -> SegmentOfResult) -> FlattenCursor<MapCursor<Self, SegmentOfResult>> { + return map(transform).joined() + } + + /// Calls the given closure on each element in the cursor. + public func forEach(_ body: (Element) throws -> Void) throws { + while let element = try next() { + try body(element) + } + } + + /// Returns a cursor over the results of the transform function applied to + /// this cursor's elements. + public func map<T>(_ transform: @escaping (Element) throws -> T) -> MapCursor<Self, T> { + return MapCursor(self, transform) + } + + /// Returns the result of calling the given combining closure with each + /// element of this sequence and an accumulating value. + public func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) throws -> Result) throws -> Result { + var result = initialResult + while let element = try next() { + result = try nextPartialResult(result, element) + } + return result + } +} + +extension Cursor where Element: Equatable { + /// Returns a Boolean value indicating whether the cursor contains the + /// given element. + public func contains(_ element: Element) throws -> Bool { + while let e = try next() { + if e == element { + return true + } + } + return false + } +} + +extension Cursor where Element: Cursor { + /// Returns the elements of this cursor of cursors, concatenated. + public func joined() -> FlattenCursor<Self> { + return FlattenCursor(self) + } +} + +extension Cursor where Element: Sequence { + /// Returns the elements of this cursor of sequences, concatenated. + public func joined() -> FlattenCursor<MapCursor<Self, IteratorCursor<Self.Element.Iterator>>> { + return flatMap { $0 } + } +} + +/// An enumeration of the elements of a cursor. +/// +/// To create an instance of `EnumeratedCursor`, call the `enumerated()` method +/// on a cursor: +/// +/// let cursor = try String.fetchCursor(db, "SELECT 'foo' UNION ALL SELECT 'bar'") +/// let c = cursor.enumerated() +/// while let (n, x) = c.next() { +/// print("\(n): \(x)") +/// } +/// // Prints: "0: foo" +/// // Prints: "1: bar" +public final class EnumeratedCursor<Base: Cursor> : Cursor { + init(_ base: Base) { + self.index = 0 + self.base = base + } + + /// Advances to the next element and returns it, or nil if no next + /// element exists. + public func next() throws -> (Int, Base.Element)? { + guard let element = try base.next() else { return nil } + defer { index += 1 } + return (index, element) + } + + private var index: Int + private var base: Base +} + +/// A cursor whose elements consist of the elements of some base cursor that +/// also satisfy a given predicate. +public final class FilterCursor<Base: Cursor> : Cursor { + init(_ base: Base, _ isIncluded: @escaping (Base.Element) throws -> Bool) { + self.base = base + self.isIncluded = isIncluded + } + + /// Advances to the next element and returns it, or nil if no next + /// element exists. + public func next() throws -> Base.Element? { + while let element = try base.next() { + if try isIncluded(element) { + return element + } + } + return nil + } + + private let base: Base + private let isIncluded: (Base.Element) throws -> Bool +} + +/// A cursor consisting of all the elements contained in each segment contained +/// in some Base cursor. +/// +/// See Cursor.joined(), Cursor.flatMap(_:), Sequence.flatMap(_:) +public final class FlattenCursor<Base: Cursor> : Cursor where Base.Element: Cursor { + init(_ base: Base) { + self.base = base + } + + /// Advances to the next element and returns it, or nil if no next + /// element exists. + public func next() throws -> Base.Element.Element? { + while true { + if let element = try inner?.next() { + return element + } + guard let inner = try base.next() else { + return nil + } + self.inner = inner + } + } + + private var inner: Base.Element? + private let base: Base +} + +/// A Cursor whose elements consist of those in a Base Cursor passed through a +/// transform function returning Element. +/// +/// See Cursor.map(_:) +public final class MapCursor<Base: Cursor, Element> : Cursor { + init(_ base: Base, _ transform: @escaping (Base.Element) throws -> Element) { + self.base = base + self.transform = transform + } + + /// Advances to the next element and returns it, or nil if no next + /// element exists. + public func next() throws -> Element? { + guard let element = try base.next() else { return nil } + return try transform(element) + } + + private let base: Base + private let transform: (Base.Element) throws -> Element +} + +/// A Cursor whose elements are those of a sequence iterator. +public final class IteratorCursor<Base: IteratorProtocol> : Cursor { + + /// Creates a cursor from a sequence iterator. + public init(_ base: Base) { + self.base = base + } + + /// Creates a cursor from a sequence. + public init<S: Sequence>(_ s: S) where S.Iterator == Base { + self.base = s.makeIterator() + } + + /// Advances to the next element and returns it, or nil if no next + /// element exists. + public func next() -> Base.Element? { + return base.next() + } + + private var base: Base +} + +extension Sequence { + + /// Returns a cursor over the concatenated results of mapping transform + /// over self. + public func flatMap<SegmentOfResult: Cursor>(_ transform: @escaping (Iterator.Element) throws -> SegmentOfResult) -> FlattenCursor<MapCursor<IteratorCursor<Self.Iterator>, SegmentOfResult>> { + return IteratorCursor(self).flatMap(transform) + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/Database.swift b/Pods/GRDB.swift/GRDB/Core/Database.swift new file mode 100644 index 0000000..2588106 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Database.swift @@ -0,0 +1,2758 @@ +import Foundation +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif + +/// A raw SQLite connection, suitable for the SQLite C API. +public typealias SQLiteConnection = OpaquePointer + +/// A raw SQLite function argument. +typealias SQLiteValue = OpaquePointer + +let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_destructor_type.self) + +/// A Database connection. +/// +/// You don't create a database directly. Instead, you use a DatabaseQueue, or +/// a DatabasePool: +/// +/// let dbQueue = DatabaseQueue(...) +/// +/// // The Database is the `db` in the closure: +/// try dbQueue.inDatabase { db in +/// try db.execute(...) +/// } +public final class Database { + // The Database class is not thread-safe. An instance should always be + // used through a SerializedDatabase. + + // MARK: - Database Types + + /// See BusyMode and https://www.sqlite.org/c3ref/busy_handler.html + public typealias BusyCallback = (_ numberOfTries: Int) -> Bool + + /// When there are several connections to a database, a connection may try + /// to access the database while it is locked by another connection. + /// + /// The BusyMode enum describes the behavior of GRDB when such a situation + /// occurs: + /// + /// - .immediateError: The SQLITE_BUSY error is immediately returned to the + /// connection that tries to access the locked database. + /// + /// - .timeout: The SQLITE_BUSY error will be returned only if the database + /// remains locked for more than the specified duration. + /// + /// - .callback: Perform your custom lock handling. + /// + /// To set the busy mode of a database, use Configuration: + /// + /// let configuration = Configuration(busyMode: .timeout(1)) + /// let dbQueue = DatabaseQueue(path: "...", configuration: configuration) + /// + /// Relevant SQLite documentation: + /// + /// - https://www.sqlite.org/c3ref/busy_timeout.html + /// - https://www.sqlite.org/c3ref/busy_handler.html + /// - https://www.sqlite.org/lang_transaction.html + /// - https://www.sqlite.org/wal.html + public enum BusyMode { + /// The SQLITE_BUSY error is immediately returned to the connection that + /// tries to access the locked database. + case immediateError + + /// The SQLITE_BUSY error will be returned only if the database remains + /// locked for more than the specified duration. + case timeout(TimeInterval) + + /// A custom callback that is called when a database is locked. + /// See https://www.sqlite.org/c3ref/busy_handler.html + case callback(BusyCallback) + } + + /// The available [checkpoint modes](https://www.sqlite.org/c3ref/wal_checkpoint_v2.html). + public enum CheckpointMode: Int32 { + case passive = 0 // SQLITE_CHECKPOINT_PASSIVE + case full = 1 // SQLITE_CHECKPOINT_FULL + case restart = 2 // SQLITE_CHECKPOINT_RESTART + case truncate = 3 // SQLITE_CHECKPOINT_TRUNCATE + } + + /// A built-in SQLite collation. + /// + /// See https://www.sqlite.org/datatype3.html#collation + public struct CollationName : RawRepresentable, Hashable { + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + public init(_ rawValue: String) { + self.rawValue = rawValue + } + + /// The hash value + public var hashValue: Int { + return rawValue.hashValue + } + + /// The `BINARY` built-in SQL collation + public static let binary = CollationName("BINARY") + + /// The `NOCASE` built-in SQL collation + public static let nocase = CollationName("NOCASE") + + /// The `RTRIM` built-in SQL collation + public static let rtrim = CollationName("RTRIM") + } + + /// An SQLite conflict resolution. + /// + /// See https://www.sqlite.org/lang_conflict.html. + public enum ConflictResolution : String { + case rollback = "ROLLBACK" + case abort = "ABORT" + case fail = "FAIL" + case ignore = "IGNORE" + case replace = "REPLACE" + } + + /// An SQL column type. + /// + /// try db.create(table: "players") { t in + /// t.column("id", .integer).primaryKey() + /// t.column("title", .text) + /// } + /// + /// See https://www.sqlite.org/datatype3.html + public struct ColumnType : RawRepresentable, Hashable { + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + public init(_ rawValue: String) { + self.rawValue = rawValue + } + + /// The hash value + public var hashValue: Int { + return rawValue.hashValue + } + + /// The `TEXT` SQL column type + public static let text = ColumnType("TEXT") + + /// The `INTEGER` SQL column type + public static let integer = ColumnType("INTEGER") + + /// The `DOUBLE` SQL column type + public static let double = ColumnType("DOUBLE") + + /// The `NUMERIC` SQL column type + public static let numeric = ColumnType("NUMERIC") + + /// The `BOOLEAN` SQL column type + public static let boolean = ColumnType("BOOLEAN") + + /// The `BLOB` SQL column type + public static let blob = ColumnType("BLOB") + + /// The `DATE` SQL column type + public static let date = ColumnType("DATE") + + /// The `DATETIME` SQL column type + public static let datetime = ColumnType("DATETIME") + } + + + /// A foreign key action. + /// + /// See https://www.sqlite.org/foreignkeys.html + public enum ForeignKeyAction : String { + case cascade = "CASCADE" + case restrict = "RESTRICT" + case setNull = "SET NULL" + case setDefault = "SET DEFAULT" + } + + /// An SQLite threading mode. See https://www.sqlite.org/threadsafe.html. + enum ThreadingMode { + case `default` + case multiThread + case serialized + + var SQLiteOpenFlags: Int32 { + switch self { + case .`default`: + return 0 + case .multiThread: + return SQLITE_OPEN_NOMUTEX + case .serialized: + return SQLITE_OPEN_FULLMUTEX + } + } + } + + /// An SQLite transaction kind. See https://www.sqlite.org/lang_transaction.html + public enum TransactionKind { + case deferred + case immediate + case exclusive + } + + /// The end of a transaction: Commit, or Rollback + public enum TransactionCompletion { + case commit + case rollback + } + + /// The states that keep track of transaction completions in order to notify + /// transaction observers. + private enum TransactionHookState { + case pending + case commit + case rollback + case cancelledCommit(Error) + } + + // MARK: - Error Log + + /// log function that takes an error message. + public typealias LogErrorFunction = (_ resultCode: ResultCode, _ message: String) -> Void + + /// The error logging function. + /// + /// Quoting https://www.sqlite.org/errlog.html: + /// + /// > SQLite can be configured to invoke a callback function containing an + /// > error code and a terse error message whenever anomalies occur. This + /// > mechanism is very helpful in tracking obscure problems that occur + /// > rarely and in the field. Application developers are encouraged to take + /// > advantage of the error logging facility of SQLite in their products, + /// > as it is very low CPU and memory cost but can be a huge aid + /// > for debugging. + public static var logError: LogErrorFunction? = nil + + private static func setupErrorLog() { + struct Impl { + static let setupErrorLog: () = { + registerErrorLogCallback { (_, code, message) in + guard let log = Database.logError else { return } + guard let message = message.map({ String(cString: $0) }) else { return } + let resultCode = ResultCode(rawValue: code) + log(resultCode, message) + } + }() + } + Impl.setupErrorLog + } + + + // MARK: - Database Information + + /// The list of compile options used when building SQLite + static let sqliteCompileOptions: Set<String> = DatabaseQueue().inDatabase { try! Set(String.fetchCursor($0, "PRAGMA COMPILE_OPTIONS")) } + + /// The database configuration + public let configuration: Configuration + + /// The raw SQLite connection, suitable for the SQLite C API. + public let sqliteConnection: SQLiteConnection + + /// The rowID of the most recently inserted row. + /// + /// If no row has ever been inserted using this database connection, + /// returns zero. + /// + /// For more detailed information, see https://www.sqlite.org/c3ref/last_insert_rowid.html + public var lastInsertedRowID: Int64 { + SchedulingWatchdog.preconditionValidQueue(self) + return sqlite3_last_insert_rowid(sqliteConnection) + } + + /// The number of rows modified, inserted or deleted by the most recent + /// successful INSERT, UPDATE or DELETE statement. + /// + /// For more detailed information, see https://www.sqlite.org/c3ref/changes.html + public var changesCount: Int { + SchedulingWatchdog.preconditionValidQueue(self) + return Int(sqlite3_changes(sqliteConnection)) + } + + /// The total number of rows modified, inserted or deleted by all successful + /// INSERT, UPDATE or DELETE statements since the database connection was + /// opened. + /// + /// For more detailed information, see https://www.sqlite.org/c3ref/total_changes.html + public var totalChangesCount: Int { + SchedulingWatchdog.preconditionValidQueue(self) + return Int(sqlite3_total_changes(sqliteConnection)) + } + + var lastErrorCode: ResultCode { return ResultCode(rawValue: sqlite3_errcode(sqliteConnection)) } + var lastErrorMessage: String? { return String(cString: sqlite3_errmsg(sqliteConnection)) } + + /// True if the database connection is currently in a transaction. + public var isInsideTransaction: Bool { + // https://sqlite.org/c3ref/get_autocommit.html + // + // > The sqlite3_get_autocommit() interface returns non-zero or zero if + // > the given database connection is or is not in autocommit mode, + // > respectively. + // + // > Autocommit mode is on by default. Autocommit mode is disabled by a + // > BEGIN statement. Autocommit mode is re-enabled by a COMMIT + // > or ROLLBACK. + return sqlite3_get_autocommit(sqliteConnection) == 0 + } + + /// Set by SAVEPOINT/COMMIT/ROLLBACK/RELEASE savepoint statements. + private var savepointStack = SavepointStack() + + /// Traces transaction hooks + private var transactionHookState: TransactionHookState = .pending + + /// Transaction observers + private var transactionObservers = [ManagedTransactionObserver]() + private var activeTransactionObservers = [ManagedTransactionObserver]() // subset of transactionObservers, set in updateStatementWillExecute + + /// See setupBusyMode() + private var busyCallback: BusyCallback? + + /// Available functions + private var functions = Set<DatabaseFunction>() + + /// Available collations + private var collations = Set<DatabaseCollation>() + + /// Schema Cache + var schemaCache: DatabaseSchemaCache // internal so that it can be tested + + /// Statement caches are not part of the schema cache because statements + /// belong to this connection, while schema cache can be shared with + /// other connections. + /// + /// There are two statement caches: one for statements generated by the + /// user, and one for the statements generated by GRDB. Those are separated + /// so that GRDB has no opportunity to inadvertently modify the arguments of + /// user statements. + enum StatementCacheName { + case grdb + case user + } + private lazy var grdbStatementCache: StatementCache = StatementCache(database: self) + private lazy var userStatementCache: StatementCache = StatementCache(database: self) + + init(path: String, configuration: Configuration, schemaCache: DatabaseSchemaCache) throws { + // Error log setup must happen before any database connection + Database.setupErrorLog() + + // See https://www.sqlite.org/c3ref/open.html + var sqliteConnection: SQLiteConnection? = nil + let code = sqlite3_open_v2(path, &sqliteConnection, configuration.SQLiteOpenFlags, nil) + guard code == SQLITE_OK else { + throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection))) + } + + do { + // Use extended result codes + do { + let code = sqlite3_extended_result_codes(sqliteConnection!, 1) + guard code == SQLITE_OK else { + throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection))) + } + } + + #if SQLITE_HAS_CODEC + // https://discuss.zetetic.net/t/important-advisory-sqlcipher-with-xcode-8-and-new-sdks/1688 + // + // > In order to avoid situations where SQLite might be used + // > improperly at runtime, we strongly recommend that + // > applications institute a runtime test to ensure that the + // > application is actually using SQLCipher on the active + // > connection. + + let isSQLCipherValid: Bool + do { + var sqliteStatement: SQLiteStatement? = nil + let code = sqlite3_prepare_v2(sqliteConnection, "PRAGMA cipher_version", -1, &sqliteStatement, nil) + guard code == SQLITE_OK else { + throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection))) + } + defer { + sqlite3_finalize(sqliteStatement) + } + switch sqlite3_step(sqliteStatement) { + case SQLITE_ROW: + isSQLCipherValid = (sqlite3_column_text(sqliteStatement, 0) != nil) + default: + isSQLCipherValid = false + } + } + + guard isSQLCipherValid else { + throw DatabaseError(resultCode: .SQLITE_MISUSE, message: "GRDB is not linked against SQLCipher. Check https://discuss.zetetic.net/t/important-advisory-sqlcipher-with-xcode-8-and-new-sdks/1688") + } + + if let passphrase = configuration.passphrase { + try Database.set(passphrase: passphrase, forConnection: sqliteConnection!) + } + #endif + + // Users are surprised when they open a picture as a database and + // see no error (https://github.com/groue/GRDB.swift/issues/54). + // + // So let's fail early if file is not a database, or encrypted with + // another passphrase. + do { + let code = sqlite3_exec(sqliteConnection, "SELECT * FROM sqlite_master LIMIT 1", nil, nil, nil) + guard code == SQLITE_OK else { + throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection))) + } + } + } catch { + Database.close(connection: sqliteConnection!) + throw error + } + + self.configuration = configuration + self.schemaCache = schemaCache + self.sqliteConnection = sqliteConnection! + + configuration.SQLiteConnectionDidOpen?() + } + + /// This method must be called after database initialization + func setup() throws { + // Setup trace first, so that setup queries are traced. + setupTrace() + try setupForeignKeys() + setupBusyMode() + setupDefaultFunctions() + setupDefaultCollations() + setupTransactionHooks() + installDefaultAuthorizer() + } + + /// This method must be called before database deallocation + func close() { + SchedulingWatchdog.preconditionValidQueue(self) + assert(!isClosed) + + configuration.SQLiteConnectionWillClose?(sqliteConnection) + grdbStatementCache.clear() + userStatementCache.clear() + Database.close(connection: sqliteConnection) + isClosed = true + configuration.SQLiteConnectionDidClose?() + } + + private var isClosed: Bool = false + deinit { + assert(isClosed) + } + + func releaseMemory() { + sqlite3_db_release_memory(sqliteConnection) + schemaCache.clear() + grdbStatementCache.clear() + userStatementCache.clear() + } + + private func setupForeignKeys() throws { + // Foreign keys are disabled by default with SQLite3 + if configuration.foreignKeysEnabled { + try execute("PRAGMA foreign_keys = ON") + } + } + + private func setupTrace() { + guard configuration.trace != nil else { + return + } + let dbPointer = Unmanaged.passUnretained(self).toOpaque() + // sqlite3_trace_v2 and sqlite3_expanded_sql were introduced in SQLite 3.14.0 http://www.sqlite.org/changes.html#version_3_14 + // It is available from iOS 10.0 and OS X 10.12 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS) + #if GRDBCUSTOMSQLITE + sqlite3_trace_v2(sqliteConnection, UInt32(SQLITE_TRACE_STMT), { (mask, dbPointer, stmt, unexpandedSQL) -> Int32 in + guard let stmt = stmt else { return SQLITE_OK } + guard let expandedSQLCString = sqlite3_expanded_sql(OpaquePointer(stmt)) else { return SQLITE_OK } + let sql = String(cString: expandedSQLCString) + sqlite3_free(expandedSQLCString) + let db = Unmanaged<Database>.fromOpaque(dbPointer!).takeUnretainedValue() + db.configuration.trace!(sql) + return SQLITE_OK + }, dbPointer) + #elseif GRDBCIPHER + sqlite3_trace(sqliteConnection, { (dbPointer, sql) in + guard let sql = sql.map({ String(cString: $0) }) else { return } + let db = Unmanaged<Database>.fromOpaque(dbPointer!).takeUnretainedValue() + db.configuration.trace!(sql) + }, dbPointer) + #else + if #available(iOS 10.0, OSX 10.12, watchOS 3.0, *) { + sqlite3_trace_v2(sqliteConnection, UInt32(SQLITE_TRACE_STMT), { (mask, dbPointer, stmt, unexpandedSQL) -> Int32 in + guard let stmt = stmt else { return SQLITE_OK } + guard let expandedSQLCString = sqlite3_expanded_sql(OpaquePointer(stmt)) else { return SQLITE_OK } + let sql = String(cString: expandedSQLCString) + sqlite3_free(expandedSQLCString) + let db = Unmanaged<Database>.fromOpaque(dbPointer!).takeUnretainedValue() + db.configuration.trace!(sql) + return SQLITE_OK + }, dbPointer) + } else { + sqlite3_trace(sqliteConnection, { (dbPointer, sql) in + guard let sql = sql.map({ String(cString: $0) }) else { return } + let db = Unmanaged<Database>.fromOpaque(dbPointer!).takeUnretainedValue() + db.configuration.trace!(sql) + }, dbPointer) + } + #endif + } + + private func setupBusyMode() { + switch configuration.busyMode { + case .immediateError: + break + + case .timeout(let duration): + let milliseconds = Int32(duration * 1000) + sqlite3_busy_timeout(sqliteConnection, milliseconds) + + case .callback(let callback): + busyCallback = callback + let dbPointer = Unmanaged.passUnretained(self).toOpaque() + sqlite3_busy_handler( + sqliteConnection, + { (dbPointer, numberOfTries) in + let db = Unmanaged<Database>.fromOpaque(dbPointer!).takeUnretainedValue() + let callback = db.busyCallback! + return callback(Int(numberOfTries)) ? 1 : 0 + }, + dbPointer) + } + } + + private func setupDefaultFunctions() { + add(function: .capitalize) + add(function: .lowercase) + add(function: .uppercase) + + if #available(iOS 9.0, OSX 10.11, watchOS 3.0, *) { + add(function: .localizedCapitalize) + add(function: .localizedLowercase) + add(function: .localizedUppercase) + } + } + + private func setupDefaultCollations() { + add(collation: .unicodeCompare) + add(collation: .caseInsensitiveCompare) + add(collation: .localizedCaseInsensitiveCompare) + add(collation: .localizedCompare) + add(collation: .localizedStandardCompare) + } + + private func setupTransactionHooks() { + let dbPointer = Unmanaged.passUnretained(self).toOpaque() + + sqlite3_commit_hook(sqliteConnection, { dbPointer in + let db = Unmanaged<Database>.fromOpaque(dbPointer!).takeUnretainedValue() + do { + try db.willCommit() + db.transactionHookState = .commit + // Next step: updateStatementDidExecute() + return 0 + } catch { + db.transactionHookState = .cancelledCommit(error) + // Next step: sqlite3_rollback_hook callback + return 1 + } + }, dbPointer) + + + sqlite3_rollback_hook(sqliteConnection, { dbPointer in + let db = Unmanaged<Database>.fromOpaque(dbPointer!).takeUnretainedValue() + switch db.transactionHookState { + case .cancelledCommit: + // Next step: updateStatementDidFail() + break + default: + db.transactionHookState = .rollback + // Next step: updateStatementDidExecute() + } + }, dbPointer) + } + + fileprivate func installDefaultAuthorizer() { + // https://www.sqlite.org/capi3ref.html#sqlite3_set_authorizer + // > When sqlite3_prepare_v2() is used to prepare a statement, the + // > statement might be re-prepared during sqlite3_step() due to a + // > schema change. Hence, the application should ensure that the + // > correct authorizer callback remains in place during the + // > sqlite3_step(). + // + // As a matter of fact, without this default authorizer, the truncate + // optimization prevents transaction observers from observing + // individual deletions. + sqlite3_set_authorizer(sqliteConnection, { (_, actionCode, cString1, cString2, cString3, cString4) -> Int32 in + if actionCode == SQLITE_DELETE && String(cString: cString1!) != "sqlite_master" { + // Prevent [truncate optimization](https://www.sqlite.org/lang_delete.html#truncateopt) + // so that transaction observers can observe individual deletions. + return SQLITE_IGNORE + } else { + return SQLITE_OK + } + }, nil) + } + + private static func close(connection sqliteConnection: SQLiteConnection) { + #if GRDBCUSTOMSQLITE || GRDBCIPHER + close_v2(connection: sqliteConnection) + #else + if #available(iOS 8.2, OSX 10.10, OSXApplicationExtension 10.10, *) { + close_v2(connection: sqliteConnection) + } else { + // https://www.sqlite.org/c3ref/close.html + // > If the database connection is associated with unfinalized prepared + // > statements or unfinished sqlite3_backup objects then + // > sqlite3_close() will leave the database connection open and + // > return SQLITE_BUSY. + let code = sqlite3_close(sqliteConnection) + if code != SQLITE_OK, let log = logError { + // A rare situation where GRDB doesn't fatalError on + // unprocessed errors. + let message = String(cString: sqlite3_errmsg(sqliteConnection)) + log(ResultCode(rawValue: code), "could not close database: \(message)") + if code == SQLITE_BUSY { + // Let the user know about unfinalized statements that did + // prevent the connection from closing properly. + var stmt: SQLiteStatement? = sqlite3_next_stmt(sqliteConnection, nil) + while stmt != nil { + log(ResultCode(rawValue: code), "unfinalized statement: \(String(cString: sqlite3_sql(stmt)))") + stmt = sqlite3_next_stmt(sqliteConnection, stmt) + } + } + } + } + #endif + } + + // sqlite3_close_v2 was added in SQLite 3.7.14 http://www.sqlite.org/changes.html#version_3_7_14 + // It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS) + #if GRDBCUSTOMSQLITE || GRDBCIPHER + private static func close_v2(connection sqliteConnection: SQLiteConnection) { + // https://www.sqlite.org/c3ref/close.html + // > If sqlite3_close_v2() is called with unfinalized prepared + // > statements and/or unfinished sqlite3_backups, then the database + // > connection becomes an unusable "zombie" which will automatically + // > be deallocated when the last prepared statement is finalized or the + // > last sqlite3_backup is finished. + let code = sqlite3_close_v2(sqliteConnection) + if code != SQLITE_OK, let log = logError { + // A rare situation where GRDB doesn't fatalError on + // unprocessed errors. + let message = String(cString: sqlite3_errmsg(sqliteConnection)) + log(ResultCode(rawValue: code), "could not close database: \(message)") + } + } + #else + @available(iOS 8.2, OSX 10.10, OSXApplicationExtension 10.10, *) + private static func close_v2(connection sqliteConnection: SQLiteConnection) { + // https://www.sqlite.org/c3ref/close.html + // > If sqlite3_close_v2() is called with unfinalized prepared + // > statements and/or unfinished sqlite3_backups, then the database + // > connection becomes an unusable "zombie" which will automatically + // > be deallocated when the last prepared statement is finalized or the + // > last sqlite3_backup is finished. + let code = sqlite3_close_v2(sqliteConnection) + if code != SQLITE_OK, let log = logError { + // A rare situation where GRDB doesn't fatalError on + // unprocessed errors. + let message = String(cString: sqlite3_errmsg(sqliteConnection)) + log(ResultCode(rawValue: code), "could not close database: \(message)") + } + } + #endif +} + + +// ========================================================================= +// MARK: - Statements + +extension Database { + + /// Returns a new prepared statement that can be reused. + /// + /// let statement = try db.makeSelectStatement("SELECT COUNT(*) FROM players WHERE score > ?") + /// let moreThanTwentyCount = try Int.fetchOne(statement, arguments: [20])! + /// let moreThanThirtyCount = try Int.fetchOne(statement, arguments: [30])! + /// + /// - parameter sql: An SQL query. + /// - returns: A SelectStatement. + /// - throws: A DatabaseError whenever SQLite could not parse the sql query. + public func makeSelectStatement(_ sql: String) throws -> SelectStatement { + return try makeSelectStatement(sql, prepFlags: 0) + } + + /// Returns a new prepared statement that can be reused. + /// + /// let statement = try db.makeSelectStatement("SELECT COUNT(*) FROM players WHERE score > ?", prepFlags: 0) + /// let moreThanTwentyCount = try Int.fetchOne(statement, arguments: [20])! + /// let moreThanThirtyCount = try Int.fetchOne(statement, arguments: [30])! + /// + /// - parameter sql: An SQL query. + /// - parameter prepFlags: Flags for sqlite3_prepare_v3 (available from + /// SQLite 3.20.0, see http://www.sqlite.org/c3ref/prepare.html) + /// - returns: A SelectStatement. + /// - throws: A DatabaseError whenever SQLite could not parse the sql query. + func makeSelectStatement(_ sql: String, prepFlags: Int32) throws -> SelectStatement { + return try SelectStatement(database: self, sql: sql, prepFlags: prepFlags) + } + + /// Returns a prepared statement that can be reused. + /// + /// let statement = try db.cachedSelectStatement("SELECT COUNT(*) FROM players WHERE score > ?") + /// let moreThanTwentyCount = try Int.fetchOne(statement, arguments: [20])! + /// let moreThanThirtyCount = try Int.fetchOne(statement, arguments: [30])! + /// + /// The returned statement may have already been used: it may or may not + /// contain values for its eventual arguments. + /// + /// - parameter sql: An SQL query. + /// - returns: An UpdateStatement. + /// - throws: A DatabaseError whenever SQLite could not parse the sql query. + public func cachedSelectStatement(_ sql: String) throws -> SelectStatement { + return try selectStatement(sql, fromCache: .user) + } + + /// Returns a prepared statement that can be reused. + func selectStatement(_ sql: String, fromCache cacheName: StatementCacheName) throws -> SelectStatement { + switch cacheName { + case .grdb: return try grdbStatementCache.selectStatement(sql) + case .user: return try userStatementCache.selectStatement(sql) + } + } + + /// Returns a new prepared statement that can be reused. + /// + /// let statement = try db.makeUpdateStatement("INSERT INTO players (name) VALUES (?)") + /// try statement.execute(arguments: ["Arthur"]) + /// try statement.execute(arguments: ["Barbara"]) + /// + /// - parameter sql: An SQL query. + /// - returns: An UpdateStatement. + /// - throws: A DatabaseError whenever SQLite could not parse the sql query. + public func makeUpdateStatement(_ sql: String) throws -> UpdateStatement { + return try makeUpdateStatement(sql, prepFlags: 0) + } + + /// Returns a new prepared statement that can be reused. + /// + /// let statement = try db.makeUpdateStatement("INSERT INTO players (name) VALUES (?)", prepFlags: 0) + /// try statement.execute(arguments: ["Arthur"]) + /// try statement.execute(arguments: ["Barbara"]) + /// + /// - parameter sql: An SQL query. + /// - parameter prepFlags: Flags for sqlite3_prepare_v3 (available from + /// SQLite 3.20.0, see http://www.sqlite.org/c3ref/prepare.html) + /// - returns: An UpdateStatement. + /// - throws: A DatabaseError whenever SQLite could not parse the sql query. + func makeUpdateStatement(_ sql: String, prepFlags: Int32) throws -> UpdateStatement { + return try UpdateStatement(database: self, sql: sql, prepFlags: prepFlags) + } + + /// Returns a prepared statement that can be reused. + /// + /// let statement = try db.cachedUpdateStatement("INSERT INTO players (name) VALUES (?)") + /// try statement.execute(arguments: ["Arthur"]) + /// try statement.execute(arguments: ["Barbara"]) + /// + /// The returned statement may have already been used: it may or may not + /// contain values for its eventual arguments. + /// + /// - parameter sql: An SQL query. + /// - returns: An UpdateStatement. + /// - throws: A DatabaseError whenever SQLite could not parse the sql query. + public func cachedUpdateStatement(_ sql: String) throws -> UpdateStatement { + return try updateStatement(sql, fromCache: .grdb) + } + + /// Returns a prepared statement that can be reused. + func updateStatement(_ sql: String, fromCache cacheName: StatementCacheName) throws -> UpdateStatement { + switch cacheName { + case .grdb: return try grdbStatementCache.updateStatement(sql) + case .user: return try userStatementCache.updateStatement(sql) + } + } + + /// Executes one or several SQL statements, separated by semi-colons. + /// + /// try db.execute( + /// "INSERT INTO players (name) VALUES (:name)", + /// arguments: ["name": "Arthur"]) + /// + /// try db.execute(""" + /// INSERT INTO players (name) VALUES (?); + /// INSERT INTO players (name) VALUES (?); + /// INSERT INTO players (name) VALUES (?); + /// """, arguments; ['Arthur', 'Barbara', 'Craig']) + /// + /// This method may throw a DatabaseError. + /// + /// - parameters: + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - throws: A DatabaseError whenever an SQLite error occurs. + public func execute(_ sql: String, arguments: StatementArguments? = nil) throws { + // This method is like sqlite3_exec (https://www.sqlite.org/c3ref/exec.html) + // It adds support for arguments. + + SchedulingWatchdog.preconditionValidQueue(self) + + // The tricky part is to consume arguments as statements are executed. + // + // Here we build two functions: + // - consumeArguments returns arguments for a statement + // - validateRemainingArguments validates the remaining arguments, after + // all statements have been executed, in the same way + // as Statement.validate(arguments:) + + var arguments = arguments ?? StatementArguments() + let initialValuesCount = arguments.values.count + let consumeArguments = { (statement: UpdateStatement) throws -> StatementArguments in + let bindings = try arguments.consume(statement, allowingRemainingValues: true) + return StatementArguments(bindings) + } + let validateRemainingArguments = { + if !arguments.values.isEmpty { + throw DatabaseError(resultCode: .SQLITE_MISUSE, message: "wrong number of statement arguments: \(initialValuesCount)") + } + } + + + // Execute statements + + let sqlCodeUnits = sql.utf8CString + var error: Error? + + // During the execution of sqlite3_prepare_v2, the observer listens to + // authorization callbacks in order to recognize "interesting" + // statements. See updateStatementDidExecute(). + let observer = StatementCompilationObserver(self) + observer.start() + + sqlCodeUnits.withUnsafeBufferPointer { codeUnits in + let sqlStart = UnsafePointer<Int8>(codeUnits.baseAddress)! + let sqlEnd = sqlStart + sqlCodeUnits.count + var statementStart = sqlStart + while statementStart < sqlEnd - 1 { + observer.reset() + var statementEnd: UnsafePointer<Int8>? = nil + var sqliteStatement: SQLiteStatement? = nil + let code = sqlite3_prepare_v2(sqliteConnection, statementStart, -1, &sqliteStatement, &statementEnd) + guard code == SQLITE_OK else { + error = DatabaseError(resultCode: code, message: lastErrorMessage, sql: sql) + break + } + + guard sqliteStatement != nil else { + // The remaining string contains only whitespace + assert(String(data: Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: statementStart), count: statementEnd! - statementStart, deallocator: .none), encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + break + } + + do { + let statement = UpdateStatement( + database: self, + sqliteStatement: sqliteStatement!, + invalidatesDatabaseSchemaCache: observer.invalidatesDatabaseSchemaCache, + transactionStatementInfo: observer.transactionStatementInfo, + databaseEventKinds: observer.databaseEventKinds) + let arguments = try consumeArguments(statement) + statement.unsafeSetArguments(arguments) + try statement.execute() + } catch let statementError { + error = statementError + break + } + + statementStart = statementEnd! + } + } + + observer.stop() + + if let error = error { + throw error + } + + // Force arguments validity: it is a programmer error to provide + // arguments that do not match the statement. + try! validateRemainingArguments() // throws if there are remaining arguments. + } +} + + +// ========================================================================= +// MARK: - Functions + +extension Database { + + /// Add or redefine an SQL function. + /// + /// let fn = DatabaseFunction("succ", argumentCount: 1) { dbValues in + /// guard let int = Int.fromDatabaseValue(dbValues[0]) else { + /// return nil + /// } + /// return int + 1 + /// } + /// db.add(function: fn) + /// try Int.fetchOne(db, "SELECT succ(1)")! // 2 + public func add(function: DatabaseFunction) { + functions.update(with: function) + function.install(in: self) + } + + /// Remove an SQL function. + public func remove(function: DatabaseFunction) { + functions.remove(function) + function.uninstall(in: self) + } +} + + +// ========================================================================= +// MARK: - Collations + +extension Database { + + /// Add or redefine a collation. + /// + /// let collation = DatabaseCollation("localized_standard") { (string1, string2) in + /// return (string1 as NSString).localizedStandardCompare(string2) + /// } + /// db.add(collation: collation) + /// try db.execute("CREATE TABLE files (name TEXT COLLATE localized_standard") + public func add(collation: DatabaseCollation) { + collations.update(with: collation) + let collationPointer = Unmanaged.passUnretained(collation).toOpaque() + let code = sqlite3_create_collation_v2( + sqliteConnection, + collation.name, + SQLITE_UTF8, + collationPointer, + { (collationPointer, length1, buffer1, length2, buffer2) -> Int32 in + let collation = Unmanaged<DatabaseCollation>.fromOpaque(collationPointer!).takeUnretainedValue() + return Int32(collation.function(length1, buffer1, length2, buffer2).rawValue) + }, nil) + guard code == SQLITE_OK else { + // Assume a GRDB bug: there is no point throwing any error. + fatalError(DatabaseError(resultCode: code, message: lastErrorMessage).description) + } + } + + /// Remove a collation. + public func remove(collation: DatabaseCollation) { + collations.remove(collation) + sqlite3_create_collation_v2( + sqliteConnection, + collation.name, + SQLITE_UTF8, + nil, nil, nil) + } +} + +/// A Collation is a string comparison function used by SQLite. +public final class DatabaseCollation { + public let name: String + let function: (Int32, UnsafeRawPointer?, Int32, UnsafeRawPointer?) -> ComparisonResult + + /// Returns a collation. + /// + /// let collation = DatabaseCollation("localized_standard") { (string1, string2) in + /// return (string1 as NSString).localizedStandardCompare(string2) + /// } + /// db.add(collation: collation) + /// try db.execute("CREATE TABLE files (name TEXT COLLATE localized_standard") + /// + /// - parameters: + /// - name: The function name. + /// - function: A function that compares two strings. + public init(_ name: String, function: @escaping (String, String) -> ComparisonResult) { + self.name = name + self.function = { (length1, buffer1, length2, buffer2) in + // Buffers are not C strings: they do not end with \0. + let string1 = String(bytesNoCopy: UnsafeMutableRawPointer(mutating: buffer1.unsafelyUnwrapped), length: Int(length1), encoding: .utf8, freeWhenDone: false)! + let string2 = String(bytesNoCopy: UnsafeMutableRawPointer(mutating: buffer2.unsafelyUnwrapped), length: Int(length2), encoding: .utf8, freeWhenDone: false)! + return function(string1, string2) + } + } +} + +extension DatabaseCollation : Hashable { + /// The hash value + public var hashValue: Int { + // We can't compute a hash since the equality is based on the opaque + // sqlite3_strnicmp SQLite function. + return 0 + } + + /// Two collations are equal if they share the same name (case insensitive) + public static func == (lhs: DatabaseCollation, rhs: DatabaseCollation) -> Bool { + // See https://www.sqlite.org/c3ref/create_collation.html + return sqlite3_stricmp(lhs.name, lhs.name) == 0 + } +} + + +// ========================================================================= +// MARK: - Encryption + +#if SQLITE_HAS_CODEC +extension Database { + private class func set(passphrase: String, forConnection sqliteConnection: SQLiteConnection) throws { + let data = passphrase.data(using: .utf8)! + let code = data.withUnsafeBytes { bytes in + sqlite3_key(sqliteConnection, bytes, Int32(data.count)) + } + guard code == SQLITE_OK else { + throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection))) + } + } + + func change(passphrase: String) throws { + // FIXME: sqlite3_rekey is discouraged. + // + // https://github.com/ccgus/fmdb/issues/547#issuecomment-259219320 + // + // > We (Zetetic) have been discouraging the use of sqlite3_rekey in + // > favor of attaching a new database with the desired encryption + // > options and using sqlcipher_export() to migrate the contents and + // > schema of the original db into the new one: + // > https://discuss.zetetic.net/t/how-to-encrypt-a-plaintext-sqlite-database-to-use-sqlcipher-and-avoid-file-is-encrypted-or-is-not-a-database-errors/ + let data = passphrase.data(using: .utf8)! + let code = data.withUnsafeBytes { bytes in + sqlite3_rekey(sqliteConnection, bytes, Int32(data.count)) + } + guard code == SQLITE_OK else { + throw DatabaseError(resultCode: code, message: lastErrorMessage) + } + } +} +#endif + + +// ========================================================================= +// MARK: - Database Schema + +extension Database { + + /// Clears the database schema cache. + /// + /// You may need to clear the cache manually if the database schema is + /// modified by another connection. + public func clearSchemaCache() { + SchedulingWatchdog.preconditionValidQueue(self) + schemaCache.clear() + + // We also clear updateStatementCache and selectStatementCache despite + // the automatic statement recompilation (see https://www.sqlite.org/c3ref/prepare.html) + // because the automatic statement recompilation only happens a + // limited number of times. + grdbStatementCache.clear() + userStatementCache.clear() + } + + /// Returns whether a table exists. + public func tableExists(_ tableName: String) throws -> Bool { + // SQlite identifiers are case-insensitive, case-preserving (http://www.alberton.info/dbms_identifiers_and_case_sensitivity.html) + return try Row.fetchOne(self, "SELECT 1 FROM (SELECT sql, type, name FROM sqlite_master UNION SELECT sql, type, name FROM sqlite_temp_master) WHERE type = 'table' AND LOWER(name) = ?", arguments: [tableName.lowercased()]) != nil + } + + /// The primary key for table named `tableName`. + /// + /// All tables have a primary key, even when it is not explicit. When a + /// table has no explicit primary key, the result is the hidden + /// "rowid" column. + /// + /// - throws: A DatabaseError if table does not exist. + public func primaryKey(_ tableName: String) throws -> PrimaryKeyInfo { + SchedulingWatchdog.preconditionValidQueue(self) + + if let primaryKey = schemaCache.primaryKey(tableName) { + return primaryKey + } + + // https://www.sqlite.org/pragma.html + // + // > PRAGMA database.table_info(table-name); + // > + // > This pragma returns one row for each column in the named table. + // > Columns in the result set include the column name, data type, + // > whether or not the column can be NULL, and the default value for + // > the column. The "pk" column in the result set is zero for columns + // > that are not part of the primary key, and is the index of the + // > column in the primary key for columns that are part of the primary + // > key. + // + // CREATE TABLE players ( + // id INTEGER PRIMARY KEY, + // name TEXT, + // score INTEGER) + // + // PRAGMA table_info("players") + // + // cid | name | type | notnull | dflt_value | pk | + // 0 | id | INTEGER | 0 | NULL | 1 | + // 1 | name | TEXT | 0 | NULL | 0 | + // 2 | score | INTEGER | 0 | NULL | 0 | + + let columns = try self.columns(in: tableName) + + let primaryKey: PrimaryKeyInfo + let pkColumns = columns + .filter { $0.primaryKeyIndex > 0 } + .sorted { $0.primaryKeyIndex < $1.primaryKeyIndex } + + switch pkColumns.count { + case 0: + // No explicit primary key => primary key is hidden rowID column + primaryKey = .hiddenRowID + case 1: + // Single column + let pkColumn = pkColumns.first! + + // https://www.sqlite.org/lang_createtable.html: + // + // > With one exception noted below, if a rowid table has a primary + // > key that consists of a single column and the declared type of + // > that column is "INTEGER" in any mixture of upper and lower + // > case, then the column becomes an alias for the rowid. Such a + // > column is usually referred to as an "integer primary key". + // > A PRIMARY KEY column only becomes an integer primary key if the + // > declared type name is exactly "INTEGER". Other integer type + // > names like "INT" or "BIGINT" or "SHORT INTEGER" or "UNSIGNED + // > INTEGER" causes the primary key column to behave as an ordinary + // > table column with integer affinity and a unique index, not as + // > an alias for the rowid. + // > + // > The exception mentioned above is that if the declaration of a + // > column with declared type "INTEGER" includes an "PRIMARY KEY + // > DESC" clause, it does not become an alias for the rowid [...] + // + // FIXME: We ignore the exception, and consider all INTEGER primary + // keys as aliases for the rowid: + if pkColumn.type.uppercased() == "INTEGER" { + primaryKey = .rowID(pkColumn.name) + } else { + primaryKey = .regular([pkColumn.name]) + } + default: + // Multi-columns primary key + primaryKey = .regular(pkColumns.map { $0.name }) + } + + schemaCache.set(primaryKey: primaryKey, forTable: tableName) + return primaryKey + } + + /// The number of columns in the table named `tableName`. + /// + /// - throws: A DatabaseError if table does not exist. + public func columnCount(in tableName: String) throws -> Int { + return try columns(in: tableName).count + } + + /// The columns in the table named `tableName`. + /// + /// - throws: A DatabaseError if table does not exist. + func columns(in tableName: String) throws -> [ColumnInfo] { + if let columns = schemaCache.columns(in: tableName) { + return columns + } + + // https://www.sqlite.org/pragma.html + // + // > PRAGMA database.table_info(table-name); + // > + // > This pragma returns one row for each column in the named table. + // > Columns in the result set include the column name, data type, + // > whether or not the column can be NULL, and the default value for + // > the column. The "pk" column in the result set is zero for columns + // > that are not part of the primary key, and is the index of the + // > column in the primary key for columns that are part of the primary + // > key. + // + // CREATE TABLE players ( + // id INTEGER PRIMARY KEY, + // firstName TEXT, + // lastName TEXT) + // + // PRAGMA table_info("players") + // + // cid | name | type | notnull | dflt_value | pk | + // 0 | id | INTEGER | 0 | NULL | 1 | + // 1 | name | TEXT | 0 | NULL | 0 | + // 2 | score | INTEGER | 0 | NULL | 0 | + + if sqlite3_libversion_number() < 3008005 { + // Work around a bug in SQLite where PRAGMA table_info would + // return a result even after the table was deleted. + if try !tableExists(tableName) { + throw DatabaseError(message: "no such table: \(tableName)") + } + } + let columns = try ColumnInfo.fetchAll(self, "PRAGMA table_info(\(tableName.quotedDatabaseIdentifier))") + guard columns.count > 0 else { + throw DatabaseError(message: "no such table: \(tableName)") + } + + schemaCache.set(columns: columns, forTable: tableName) + return columns + } + + /// The indexes on table named `tableName`; returns the empty array if the + /// table does not exist. + /// + /// Note: SQLite does not define any index for INTEGER PRIMARY KEY columns: + /// this method does not return any index that represents this primary key. + /// + /// If you want to know if a set of columns uniquely identify a row, prefer + /// table(_:hasUniqueKey:) instead. + public func indexes(on tableName: String) throws -> [IndexInfo] { + if let indexes = schemaCache.indexes(on: tableName) { + return indexes + } + + let indexes = try Row.fetchAll(self, "PRAGMA index_list(\(tableName.quotedDatabaseIdentifier))").map { row -> IndexInfo in + let indexName: String = row[1] + let unique: Bool = row[2] + let columns = try Row.fetchAll(self, "PRAGMA index_info(\(indexName.quotedDatabaseIdentifier))") + .map { ($0[0] as Int, $0[2] as String) } + .sorted { $0.0 < $1.0 } + .map { $0.1 } + return IndexInfo(name: indexName, columns: columns, unique: unique) + } + + schemaCache.set(indexes: indexes, forTable: tableName) + return indexes + } + + /// If there exists a unique key on columns, return the columns + /// ordered as the matching index (or primay key). Case of returned columns + /// is not guaranteed. + func columnsForUniqueKey<T: Sequence>(_ columns: T, in tableName: String) throws -> [String]? where T.Iterator.Element == String { + let primaryKey = try self.primaryKey(tableName) // first, so that we fail early and consistently should the table not exist + let lowercasedColumns = Set(columns.map { $0.lowercased() }) + if Set(primaryKey.columns.map { $0.lowercased() }) == lowercasedColumns { + return primaryKey.columns + } + if let index = try indexes(on: tableName).first(where: { index in index.isUnique && Set(index.columns.map { $0.lowercased() }) == lowercasedColumns }) { + // There is an explicit unique index on the columns + return index.columns + } + return nil + } + + /// True if a sequence of columns uniquely identifies a row, that is to say + /// if the columns are the primary key, or if there is a unique index on them. + public func table<T: Sequence>(_ tableName: String, hasUniqueKey columns: T) throws -> Bool where T.Iterator.Element == String { + return try columnsForUniqueKey(Array(columns), in: tableName) != nil + } + + /// The foreign keys defined on table named `tableName`. + public func foreignKeys(on tableName: String) throws -> [ForeignKeyInfo] { + if let foreignKeys = schemaCache.foreignKeys(on: tableName) { + return foreignKeys + } + + var rawForeignKeys: [(destinationTable: String, mapping: [(origin: String, destination: String?, seq: Int)])] = [] + var previousId: Int? = nil + for row in try Row.fetchAll(self, "PRAGMA foreign_key_list(\(tableName.quotedDatabaseIdentifier))") { + // row = <Row id:0 seq:0 table:"parents" from:"parentId" to:"id" on_update:"..." on_delete:"..." match:"..."> + let id: Int = row[0] + let seq: Int = row[1] + let table: String = row[2] + let origin: String = row[3] + let destination: String? = row[4] + + if previousId == id { + rawForeignKeys[rawForeignKeys.count - 1].mapping.append((origin: origin, destination: destination, seq: seq)) + } else { + rawForeignKeys.append((destinationTable: table, mapping: [(origin: origin, destination: destination, seq: seq)])) + previousId = id + } + } + + let foreignKeys = try rawForeignKeys.map { (destinationTable, columnMapping) -> ForeignKeyInfo in + let orderedMapping = columnMapping + .sorted { $0.seq < $1.seq } + .map { (origin: $0.origin, destination: $0 .destination) } + + let completeMapping: [(origin: String, destination: String)] + if orderedMapping.contains(where: { (_, destination) in destination == nil }) { + let pk = try primaryKey(destinationTable) + completeMapping = zip(pk.columns, orderedMapping).map { (pkColumn, arrow) in + (origin: arrow.origin, destination: pkColumn) + } + } else { + completeMapping = orderedMapping.map { (origin, destination) in + (origin: origin, destination: destination!) + } + } + return ForeignKeyInfo(destinationTable: destinationTable, mapping: completeMapping) + } + + schemaCache.set(foreignKeys: foreignKeys, forTable: tableName) + return foreignKeys + } +} + +/// A column of a table +struct ColumnInfo : RowConvertible { + // CREATE TABLE players ( + // id INTEGER PRIMARY KEY, + // firstName TEXT, + // lastName TEXT) + // + // PRAGMA table_info("players") + // + // cid | name | type | notnull | dflt_value | pk | + // 0 | id | INTEGER | 0 | NULL | 1 | + // 1 | name | TEXT | 0 | NULL | 0 | + // 2 | score | INTEGER | 0 | NULL | 0 | + let name: String + let type: String + let notNull: Bool + let defaultDatabaseValue: DatabaseValue + let primaryKeyIndex: Int + + init(row: Row) { + name = row["name"] + type = row["type"] + notNull = row["notnull"] + defaultDatabaseValue = row["dflt_value"] + primaryKeyIndex = row["pk"] + } +} + +/// An index on a database table. +/// +/// See `Database.indexes(on:)` +public struct IndexInfo { + /// The name of the index + public let name: String + + /// The indexed columns + public let columns: [String] + + /// True if the index is unique + public let isUnique: Bool + + init(name: String, columns: [String], unique: Bool) { + self.name = name + self.columns = columns + self.isUnique = unique + } +} + +/// Primary keys are returned from the Database.primaryKey(_:) method. +/// +/// When the table's primary key is the rowid: +/// +/// // CREATE TABLE items (name TEXT) +/// let pk = try db.primaryKey("items") +/// pk.columns // ["rowid"] +/// pk.rowIDColumn // nil +/// pk.isRowID // true +/// +/// // CREATE TABLE citizens ( +/// // id INTEGER PRIMARY KEY, +/// // name TEXT +/// // ) +/// let pk = try db.primaryKey("citizens")! +/// pk.columns // ["id"] +/// pk.rowIDColumn // "id" +/// pk.isRowID // true +/// +/// When the table's primary key is not the rowid: +/// +/// // CREATE TABLE countries ( +/// // isoCode TEXT NOT NULL PRIMARY KEY +/// // name TEXT +/// // ) +/// let pk = db.primaryKey("countries")! +/// pk.columns // ["isoCode"] +/// pk.rowIDColumn // nil +/// pk.isRowID // false +/// +/// // CREATE TABLE citizenships ( +/// // citizenID INTEGER NOT NULL REFERENCES citizens(id) +/// // countryIsoCode TEXT NOT NULL REFERENCES countries(isoCode) +/// // PRIMARY KEY (citizenID, countryIsoCode) +/// // ) +/// let pk = db.primaryKey("citizenships")! +/// pk.columns // ["citizenID", "countryIsoCode"] +/// pk.rowIDColumn // nil +/// pk.isRowID // false +public struct PrimaryKeyInfo { + private enum Impl { + /// The hidden rowID. + case hiddenRowID + + /// An INTEGER PRIMARY KEY column that aliases the Row ID. + /// Associated string is the column name. + case rowID(String) + + /// Any primary key, but INTEGER PRIMARY KEY. + /// Associated strings are column names. + case regular([String]) + } + + private let impl: Impl + + static func rowID(_ column: String) -> PrimaryKeyInfo { + return PrimaryKeyInfo(impl: .rowID(column)) + } + + static func regular(_ columns: [String]) -> PrimaryKeyInfo { + assert(!columns.isEmpty) + return PrimaryKeyInfo(impl: .regular(columns)) + } + + static let hiddenRowID = PrimaryKeyInfo(impl: .hiddenRowID) + + /// The columns in the primary key; this array is never empty. + public var columns: [String] { + switch impl { + case .hiddenRowID: + return [Column.rowID.name] + case .rowID(let column): + return [column] + case .regular(let columns): + return columns + } + } + + /// When not nil, the name of the column that contains the INTEGER PRIMARY KEY. + public var rowIDColumn: String? { + switch impl { + case .hiddenRowID: + return nil + case .rowID(let column): + return column + case .regular: + return nil + } + } + + /// When true, the primary key is the rowid: + public var isRowID: Bool { + switch impl { + case .hiddenRowID: + return true + case .rowID: + return true + case .regular: + return false + } + } +} + +/// You get foreign keys from table names, with the +/// `foreignKeys(on:)` method. +public struct ForeignKeyInfo { + /// The name of the destination table + public let destinationTable: String + + /// The column to column mapping + public let mapping: [(origin: String, destination: String)] + + /// The origin columns + public var originColumns: [String] { + return mapping.map { $0.origin } + } + + /// The destination columns + public var destinationColumns: [String] { + return mapping.map { $0.destination } + } +} + + +// ========================================================================= +// MARK: - StatementCompilationObserver + +/// A class that gathers information about a statement during its compilation. +final class StatementCompilationObserver { + let database: Database + + /// A dictionary [tablename: Set<columnName>] of accessed columns + var selectionInfo = SelectStatement.SelectionInfo() + + /// What this statement does to the database + var databaseEventKinds: [DatabaseEventKind] = [] + + /// True if a statement alter the schema in a way that required schema cache + /// invalidation. Adding a column to a table does invalidate the schema + /// cache, but not adding a table. + var invalidatesDatabaseSchemaCache = false + + /// Not nil if a statement is a BEGIN/COMMIT/ROLLBACK/RELEASE transaction/savepoint statement. + var transactionStatementInfo: UpdateStatement.TransactionStatementInfo? + + var isDropTableStatement = false + + init(_ database: Database) { + self.database = database + } + + // Call this method before calling sqlite3_prepare_v2() + func start() { + let observerPointer = Unmanaged.passUnretained(self).toOpaque() + sqlite3_set_authorizer(database.sqliteConnection, { (observerPointer, actionCode, cString1, cString2, cString3, cString4) -> Int32 in + // print("\(actionCode) \([cString1, cString2, cString3, cString4].flatMap { $0.map({ String(cString: $0) }) })") + + // https://www.sqlite.org/c3ref/set_authorizer.html: + // + // > Applications must always be prepared to encounter a NULL + // > pointer in any of the third through the sixth parameters of + // > the authorization callback. + switch actionCode { + case SQLITE_DROP_TABLE, SQLITE_DROP_TEMP_TABLE, SQLITE_DROP_TEMP_VIEW, SQLITE_DROP_VIEW, SQLITE_DETACH, SQLITE_ALTER_TABLE, SQLITE_DROP_VTABLE, SQLITE_CREATE_INDEX, SQLITE_CREATE_TEMP_INDEX, SQLITE_DROP_INDEX, SQLITE_DROP_TEMP_INDEX: + let observer = Unmanaged<StatementCompilationObserver>.fromOpaque(observerPointer!).takeUnretainedValue() + if actionCode == SQLITE_DROP_TABLE || actionCode == SQLITE_DROP_VTABLE { + observer.isDropTableStatement = true + } + observer.invalidatesDatabaseSchemaCache = true + case SQLITE_READ: + guard let tableName = cString1.map({ String(cString: $0) }) else { return SQLITE_OK } + guard let columnName = cString2.map({ String(cString: $0) }) else { return SQLITE_OK } + let observer = Unmanaged<StatementCompilationObserver>.fromOpaque(observerPointer!).takeUnretainedValue() + if columnName.isEmpty { + // SELECT COUNT(*) FROM table + observer.selectionInfo.insert(allColumnsOfTable: tableName) + } else { + // SELECT column FROM table + observer.selectionInfo.insert(column: columnName, ofTable: tableName) + } + case SQLITE_INSERT: + guard let tableName = cString1.map({ String(cString: $0) }) else { return SQLITE_OK } + let observer = Unmanaged<StatementCompilationObserver>.fromOpaque(observerPointer!).takeUnretainedValue() + observer.databaseEventKinds.append(.insert(tableName: tableName)) + case SQLITE_DELETE: + guard let tableName = cString1.map({ String(cString: $0) }) else { return SQLITE_OK } + let observer = Unmanaged<StatementCompilationObserver>.fromOpaque(observerPointer!).takeUnretainedValue() + if tableName != "sqlite_master" && !observer.isDropTableStatement { + observer.databaseEventKinds.append(.delete(tableName: tableName)) + // Prevent [truncate optimization](https://www.sqlite.org/lang_delete.html#truncateopt) + // so that transaction observers can observe individual deletions. + return SQLITE_IGNORE + } + case SQLITE_UPDATE: + guard let tableName = cString1.map({ String(cString: $0) }) else { return SQLITE_OK } + guard let columnName = cString2.map({ String(cString: $0) }) else { return SQLITE_OK } + let observer = Unmanaged<StatementCompilationObserver>.fromOpaque(observerPointer!).takeUnretainedValue() + observer.insertUpdateEventKind(tableName: tableName, columnName: columnName) + case SQLITE_TRANSACTION: + guard let rawAction = cString1.map({ String(cString: $0) }) else { return SQLITE_OK } + let observer = Unmanaged<StatementCompilationObserver>.fromOpaque(observerPointer!).takeUnretainedValue() + let action = UpdateStatement.TransactionStatementInfo.TransactionAction(rawValue: rawAction)! + observer.transactionStatementInfo = .transaction(action: action) + case SQLITE_SAVEPOINT: + guard let rawAction = cString1.map({ String(cString: $0) }) else { return SQLITE_OK } + guard let savepointName = cString2.map({ String(cString: $0) }) else { return SQLITE_OK } + let observer = Unmanaged<StatementCompilationObserver>.fromOpaque(observerPointer!).takeUnretainedValue() + let action = UpdateStatement.TransactionStatementInfo.SavepointAction(rawValue: rawAction)! + observer.transactionStatementInfo = .savepoint(name: savepointName, action: action) + case SQLITE_FUNCTION: + // Starting SQLite 3.19.0, `SELECT COUNT(*) FROM table` triggers + // an authorization callback for SQLITE_READ with an empty + // column: http://www.sqlite.org/changes.html#version_3_19_0 + // + // Before SQLite 3.19.0, `SELECT COUNT(*) FROM table` does not + // trigger any authorization callback that tells about the + // counted table: any use of the COUNT function makes the + // selection undetermined. + guard sqlite3_libversion_number() < 3019000 else { return SQLITE_OK } + guard let functionName = cString2.map({ String(cString: $0) }) else { return SQLITE_OK } + if functionName.uppercased() == "COUNT" { + let observer = Unmanaged<StatementCompilationObserver>.fromOpaque(observerPointer!).takeUnretainedValue() + observer.selectionInfo = SelectStatement.SelectionInfo.unknown() + } + default: + break + } + return SQLITE_OK + }, observerPointer) + } + + // Call this method between two calls to calling sqlite3_prepare_v2() + func reset() { + selectionInfo = SelectStatement.SelectionInfo() + databaseEventKinds = [] + invalidatesDatabaseSchemaCache = false + transactionStatementInfo = nil + isDropTableStatement = false + } + + func insertUpdateEventKind(tableName: String, columnName: String) { + for (index, eventKind) in databaseEventKinds.enumerated() { + if case .update(let t, let columnNames) = eventKind, t == tableName { + var columnNames = columnNames + columnNames.insert(columnName) + databaseEventKinds[index] = .update(tableName: tableName, columnNames: columnNames) + return + } + } + databaseEventKinds.append(.update(tableName: tableName, columnNames: [columnName])) + } + + func stop() { + // Restore default authorizer + database.installDefaultAuthorizer() + } +} + + +// ========================================================================= +// MARK: - Transactions & Savepoint + +extension Database { + + /// The extent of a transaction observation + /// + /// See Database.add(transactionObserver:extent:) + public enum TransactionObservationExtent { + /// Observation lasts until observer is deallocated + case observerLifetime + /// Observation lasts until the next transaction + case nextTransaction + /// Observation lasts until the database is closed + case databaseLifetime + } + + /// Executes a block inside a database transaction. + /// + /// try dbQueue.inDatabase do { + /// try db.inTransaction { + /// try db.execute("INSERT ...") + /// return .commit + /// } + /// } + /// + /// If the block throws an error, the transaction is rollbacked and the + /// error is rethrown. + /// + /// This method is not reentrant: you can't nest transactions. + /// + /// - parameters: + /// - kind: The transaction type (default nil). If nil, the transaction + /// type is configuration.defaultTransactionKind, which itself + /// defaults to .immediate. See https://www.sqlite.org/lang_transaction.html + /// for more information. + /// - block: A block that executes SQL statements and return either + /// .commit or .rollback. + /// - throws: The error thrown by the block. + public func inTransaction(_ kind: TransactionKind? = nil, _ block: () throws -> TransactionCompletion) throws { + // Begin transaction + try beginTransaction(kind) + + // Now that transaction has begun, we'll rollback in case of error. + // But we'll throw the first caught error, so that user knows + // what happened. + var firstError: Error? = nil + let needsRollback: Bool + do { + let completion = try block() + switch completion { + case .commit: + try commit() + needsRollback = false + case .rollback: + needsRollback = true + } + } catch { + firstError = error + needsRollback = true + } + + if needsRollback { + do { + try rollback() + } catch { + if firstError == nil { + firstError = error + } + } + } + + if let firstError = firstError { + throw firstError + } + } + + /// Executes a block inside a savepoint. + /// + /// try dbQueue.inDatabase do { + /// try db.inSavepoint { + /// try db.execute("INSERT ...") + /// return .commit + /// } + /// } + /// + /// If the block throws an error, the savepoint is rollbacked and the + /// error is rethrown. + /// + /// This method is reentrant: you can nest savepoints. + /// + /// - parameter block: A block that executes SQL statements and return + /// either .commit or .rollback. + /// - throws: The error thrown by the block. + public func inSavepoint(_ block: () throws -> TransactionCompletion) throws { + // By default, top level SQLite savepoints open a deferred transaction. + // + // But GRDB database configuration mandates a default transaction kind + // that we have to honor. + // + // So when the default GRDB transaction kind is not deferred, we open a + // transaction instead + guard isInsideTransaction || configuration.defaultTransactionKind == .deferred else { + return try inTransaction(nil, block) + } + + // If the savepoint is top-level, we'll use ROLLBACK TRANSACTION in + // order to perform the special error handling of rollbacks (see + // the rollback method). + let topLevelSavepoint = !isInsideTransaction + + // Begin savepoint + // + // We use a single name for savepoints because there is no need + // using unique savepoint names. User could still mess with them + // with raw SQL queries, but let's assume that it is unlikely that + // the user uses "grdb" as a savepoint name. + try execute("SAVEPOINT grdb") + + // Now that savepoint has begun, we'll rollback in case of error. + // But we'll throw the first caught error, so that user knows + // what happened. + var firstError: Error? = nil + let needsRollback: Bool + do { + let completion = try block() + switch completion { + case .commit: + try execute("RELEASE SAVEPOINT grdb") + needsRollback = false + case .rollback: + needsRollback = true + } + } catch { + firstError = error + needsRollback = true + } + + if needsRollback { + do { + if topLevelSavepoint { + try rollback() + } else { + // Rollback, and release the savepoint. + // Rollback alone is not enough to clear the savepoint from + // the SQLite savepoint stack. + try execute("ROLLBACK TRANSACTION TO SAVEPOINT grdb") + try execute("RELEASE SAVEPOINT grdb") + } + } catch { + if firstError == nil { + firstError = error + } + } + } + + if let firstError = firstError { + throw firstError + } + } + + func beginTransaction(_ kind: TransactionKind? = nil) throws { + switch kind ?? configuration.defaultTransactionKind { + case .deferred: + try execute("BEGIN DEFERRED TRANSACTION") + case .immediate: + try execute("BEGIN IMMEDIATE TRANSACTION") + case .exclusive: + try execute("BEGIN EXCLUSIVE TRANSACTION") + } + } + + private func rollback() throws { + // The SQLite documentation contains two related but distinct techniques + // to handle rollbacks and errors: + // + // https://www.sqlite.org/lang_transaction.html#immediate + // + // > Response To Errors Within A Transaction + // > + // > If certain kinds of errors occur within a transaction, the + // > transaction may or may not be rolled back automatically. + // > The errors that can cause an automatic rollback include: + // > + // > - SQLITE_FULL: database or disk full + // > - SQLITE_IOERR: disk I/O error + // > - SQLITE_BUSY: database in use by another process + // > - SQLITE_NOMEM: out or memory + // > + // > [...] It is recommended that applications respond to the + // > errors listed above by explicitly issuing a ROLLBACK + // > command. If the transaction has already been rolled back + // > automatically by the error response, then the ROLLBACK + // > command will fail with an error, but no harm is caused + // > by this. + // + // https://sqlite.org/c3ref/get_autocommit.html + // + // > The sqlite3_get_autocommit() interface returns non-zero or zero if + // > the given database connection is or is not in autocommit mode, + // > respectively. + // > + // > [...] If certain kinds of errors occur on a statement within a + // > multi-statement transaction (errors including SQLITE_FULL, + // > SQLITE_IOERR, SQLITE_NOMEM, SQLITE_BUSY, and SQLITE_INTERRUPT) then + // > the transaction might be rolled back automatically. The only way to + // > find out whether SQLite automatically rolled back the transaction + // > after an error is to use this function. + // + // The second technique is more robust, because we don't have to guess + // which rollback errors should be ignored, and which rollback errors + // should be exposed to the library user. + if sqlite3_get_autocommit(sqliteConnection) == 0 { + try execute("ROLLBACK TRANSACTION") + } + } + + func commit() throws { + try execute("COMMIT TRANSACTION") + } + + /// Add a transaction observer, so that it gets notified of + /// database changes. + /// + /// - parameter transactionObserver: A transaction observer. + /// - parameter extent: The duration of the observation. The default is + /// the observer lifetime (observation lasts until observer + /// is deallocated). + public func add(transactionObserver: TransactionObserver, extent: TransactionObservationExtent = .observerLifetime) { + SchedulingWatchdog.preconditionValidQueue(self) + transactionObservers.append(ManagedTransactionObserver(observer: transactionObserver, extent: extent)) + if transactionObservers.count == 1 { + installUpdateHook() + } + } + + /// Remove a transaction observer. + public func remove(transactionObserver: TransactionObserver) { + SchedulingWatchdog.preconditionValidQueue(self) + transactionObservers.removeFirst { $0.isWrapping(transactionObserver) } + if transactionObservers.isEmpty { + uninstallUpdateHook() + } + } + + /// Registers a closure to be executed after the next or current + /// transaction completion. + /// + /// dbQueue.inTransaction { db in + /// db.afterNextTransactionCommit { _ in + /// print("commit did succeed") + /// } + /// ... + /// return .commit // prints "commit did succeed" + /// } + /// + /// If the transaction is rollbacked, the closure is not executed. + /// + /// If the transaction is committed, the closure is executed in a protected + /// dispatch queue, serialized will all database updates. + public func afterNextTransactionCommit(_ closure: @escaping (Database) -> ()) { + class CommitHandler : TransactionObserver { + let closure: (Database) -> () + + init(_ closure: @escaping (Database) -> ()) { + self.closure = closure + } + + // Ignore individual changes and transaction rollbacks + func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { return false } + #if SQLITE_ENABLE_PREUPDATE_HOOK + func databaseWillChange(with event: DatabasePreUpdateEvent) { } + #endif + func databaseDidChange(with event: DatabaseEvent) { } + func databaseWillCommit() throws { } + func databaseDidRollback(_ db: Database) { } + + // On commit, run closure + func databaseDidCommit(_ db: Database) { + closure(db) + } + } + + add(transactionObserver: CommitHandler(closure), extent: .nextTransaction) + } + + /// Remove transaction observers that have stopped observing transaction, + /// and uninstall SQLite update hooks if there is no remaining observers. + private func cleanupTransactionObservers() { + transactionObservers = transactionObservers.filter { $0.isObserving } + if transactionObservers.isEmpty { + uninstallUpdateHook() + } + } + + /// Checks that a SQL query is valid for a select statement. + /// + /// Select statements do not call database.updateStatementDidExecute(). + /// Here we make sure that the update statements we track are not hidden in + /// a select statement. + /// + /// An INSERT statement will pass, but not DROP TABLE (which invalidates the + /// database cache), or RELEASE SAVEPOINT (which alters the savepoint stack) + static func preconditionValidSelectStatement(sql: String, observer: StatementCompilationObserver) { + GRDBPrecondition(observer.invalidatesDatabaseSchemaCache == false, "Invalid statement type for query \(String(reflecting: sql)): use UpdateStatement instead.") + GRDBPrecondition(observer.transactionStatementInfo == nil, "Invalid statement type for query \(String(reflecting: sql)): use UpdateStatement instead.") + + // Don't check for observer.databaseEventKinds.isEmpty + // + // When observer.databaseEventKinds.isEmpty is NOT empty, this means + // that the database is changed by the statement. + // + // It thus looks like the statement should be performed by an + // UpdateStatement, not a SelectStatement: transaction observers are not + // notified of database changes when they are executed by + // a SelectStatement. + // + // However https://github.com/groue/GRDB.swift/issues/80 and + // https://github.com/groue/GRDB.swift/issues/82 have shown that SELECT + // statements on virtual tables can generate database changes. + // + // :-( + // + // OK, this is getting very difficult to protect the user against + // himself: just give up, and allow SelectStatement to execute database + // changes. We'll cope with eventual troubles later, when they occur. + // + // GRDBPrecondition(observer.databaseEventKinds.isEmpty, "Invalid statement type for query \(String(reflecting: sql)): use UpdateStatement instead.") + } + + func updateStatementWillExecute(_ statement: UpdateStatement) { + // Grab the transaction observers that are interested in the actions + // performed by the statement. + let databaseEventKinds = statement.databaseEventKinds + activeTransactionObservers = transactionObservers.filter { observer in + return databaseEventKinds.contains(where: observer.observes) + } + } + + func selectStatementDidFail(_ statement: SelectStatement) { + // Failed statements can not be reused, because sqlite3_reset won't + // be able to restore the statement to its initial state: + // https://www.sqlite.org/c3ref/reset.html + // + // So make sure we clear this statement from the cache. + grdbStatementCache.remove(statement) + userStatementCache.remove(statement) + } + + /// Some failed statements interest transaction observers. + func updateStatementDidFail(_ statement: UpdateStatement) throws { + // Wait for next statement + activeTransactionObservers = [] + + // Reset transactionHookState before didRollback eventually executes + // other statements. + let transactionHookState = self.transactionHookState + self.transactionHookState = .pending + + // Failed statements can not be reused, because sqlite3_reset won't + // be able to restore the statement to its initial state: + // https://www.sqlite.org/c3ref/reset.html + // + // So make sure we clear this statement from the cache. + grdbStatementCache.remove(statement) + userStatementCache.remove(statement) + + switch transactionHookState { + case .rollback: + // Don't notify observers because we're in a failed implicit + // transaction here (like an INSERT which fails with + // SQLITE_CONSTRAINT error) + didRollback(notifyTransactionObservers: false) + case .cancelledCommit(let error): + didRollback(notifyTransactionObservers: true) + throw error + default: + break + } + } + + /// Some succeeded statements invalidate the database cache, others interest + /// transaction observers, and others modify the savepoint stack. + func updateStatementDidExecute(_ statement: UpdateStatement) { + // Wait for next statement + activeTransactionObservers = [] + + if statement.invalidatesDatabaseSchemaCache { + clearSchemaCache() + } + + if let transactionStatementInfo = statement.transactionStatementInfo { + switch transactionStatementInfo { + case .transaction(action: let action): + switch action { + case .begin: + break + case .commit: + if case .pending = self.transactionHookState { + // A COMMIT statement has ended a deferred transaction + // that did not open, and sqlite_commit_hook was not + // called. + // + // BEGIN DEFERRED TRANSACTION + // COMMIT + self.transactionHookState = .commit + } + case .rollback: + break + } + case .savepoint(name: let name, action: let action): + switch action { + case .begin: + savepointStack.beginSavepoint(named: name) + case .release: + savepointStack.releaseSavepoint(named: name) + if savepointStack.isEmpty { + let eventsBuffer = savepointStack.eventsBuffer + savepointStack.clear() + for (event, observers) in eventsBuffer { + for observer in observers { + event.send(to: observer) + } + } + } + case .rollback: + savepointStack.rollbackSavepoint(named: name) + } + } + } + + // Reset transactionHookState before didCommit or didRollback eventually + // execute other statements. + let transactionHookState = self.transactionHookState + self.transactionHookState = .pending + + switch transactionHookState { + case .commit: + didCommit() + case .rollback: + didRollback(notifyTransactionObservers: true) + default: + break + } + } + + /// See sqlite3_commit_hook + func willCommit() throws { + let eventsBuffer = savepointStack.eventsBuffer + savepointStack.clear() + + for (event, observers) in eventsBuffer { + for observer in observers { + event.send(to: observer) + } + } + for observer in transactionObservers { + try observer.databaseWillCommit() + } + } + +#if SQLITE_ENABLE_PREUPDATE_HOOK + /// See sqlite3_preupdate_hook + private func willChange(with event: DatabasePreUpdateEvent) { + if savepointStack.isEmpty { + // Notify all interested transactionObservers. + for observer in activeTransactionObservers { + observer.databaseWillChange(with: event) + } + } else { + // Buffer both event and the observers that should be notified of the event. + savepointStack.eventsBuffer.append((event: event.copy(), observers: activeTransactionObservers)) + } + } +#endif + + /// See sqlite3_update_hook + private func didChange(with event: DatabaseEvent) { + if savepointStack.isEmpty { + // Notify all interested transactionObservers. + for observer in activeTransactionObservers { + observer.databaseDidChange(with: event) + } + } else { + // Buffer both event and the observers that should be notified of the event. + savepointStack.eventsBuffer.append((event: event.copy(), observers: activeTransactionObservers)) + } + } + + private func didCommit() { + savepointStack.clear() + + for observer in transactionObservers { + observer.databaseDidCommit(self) + } + cleanupTransactionObservers() + } + + private func didRollback(notifyTransactionObservers: Bool) { + savepointStack.clear() + + if notifyTransactionObservers { + for observer in transactionObservers { + observer.databaseDidRollback(self) + } + } + cleanupTransactionObservers() + } + + private func installUpdateHook() { + let dbPointer = Unmanaged.passUnretained(self).toOpaque() + sqlite3_update_hook(sqliteConnection, { (dbPointer, updateKind, databaseNameCString, tableNameCString, rowID) in + let db = Unmanaged<Database>.fromOpaque(dbPointer!).takeUnretainedValue() + db.didChange(with: DatabaseEvent( + kind: DatabaseEvent.Kind(rawValue: updateKind)!, + rowID: rowID, + databaseNameCString: databaseNameCString, + tableNameCString: tableNameCString)) + }, dbPointer) + + #if SQLITE_ENABLE_PREUPDATE_HOOK + sqlite3_preupdate_hook(sqliteConnection, { (dbPointer, databaseConnection, updateKind, databaseNameCString, tableNameCString, initialRowID, finalRowID) in + let db = Unmanaged<Database>.fromOpaque(dbPointer!).takeUnretainedValue() + db.willChange(with: DatabasePreUpdateEvent( + connection: databaseConnection!, + kind: DatabasePreUpdateEvent.Kind(rawValue: updateKind)!, + initialRowID: initialRowID, + finalRowID: finalRowID, + databaseNameCString: databaseNameCString, + tableNameCString: tableNameCString)) + }, dbPointer) + #endif + } + + private func uninstallUpdateHook() { + sqlite3_update_hook(sqliteConnection, nil, nil) + #if SQLITE_ENABLE_PREUPDATE_HOOK + sqlite3_preupdate_hook(sqliteConnection, nil, nil) + #endif + } +} + + +/// A transaction observer is notified of all changes and transactions committed +/// or rollbacked on a database. +/// +/// Adopting types must be a class. +public protocol TransactionObserver : class { + + /// Filters database changes that should be notified the the + /// databaseDidChange(with:) method. + func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool + + /// Notifies a database change (insert, update, or delete). + /// + /// The change is pending until the end of the current transaction, notified + /// to databaseWillCommit, databaseDidCommit and databaseDidRollback. + /// + /// This method is called on the database queue. + /// + /// The event is only valid for the duration of this method call. If you + /// need to keep it longer, store a copy of its properties. + /// + /// - warning: this method must not change the database. + func databaseDidChange(with event: DatabaseEvent) + + /// When a transaction is about to be committed, the transaction observer + /// has an opportunity to rollback pending changes by throwing an error. + /// + /// This method is called on the database queue. + /// + /// - warning: this method must not change the database. + /// + /// - throws: An eventual error that rollbacks pending changes. + func databaseWillCommit() throws + + /// Database changes have been committed. + /// + /// This method is called on the database queue. It can change the database. + func databaseDidCommit(_ db: Database) + + /// Database changes have been rollbacked. + /// + /// This method is called on the database queue. It can change the database. + func databaseDidRollback(_ db: Database) + + #if SQLITE_ENABLE_PREUPDATE_HOOK + /// Notifies before a database change (insert, update, or delete) + /// with change information (initial / final values for the row's + /// columns). (Called *before* databaseDidChangeWithEvent.) + /// + /// The change is pending until the end of the current transaction, + /// and you always get a second chance to get basic event information in + /// the databaseDidChangeWithEvent callback. + /// + /// This callback is mostly useful for calculating detailed change + /// information for a row, and provides the initial / final values. + /// + /// This method is called on the database queue. + /// + /// The event is only valid for the duration of this method call. If you + /// need to keep it longer, store a copy of its properties. + /// + /// - warning: this method must not change the database. + /// + /// Availability Info: + /// + /// Requires SQLite 3.13.0 + + /// Compiled with option SQLITE_ENABLE_PREUPDATE_HOOK + /// + /// As of OSX 10.11.5, and iOS 9.3.2, the built-in SQLite library + /// does not have this enabled, so you'll need to compile your own + /// copy using GRDBCustomSQLite. See the README.md in /SQLiteCustom/ + /// + /// The databaseDidChangeWithEvent callback is always available, + /// and may provide most/all of what you need. + /// (For example, FetchedRecordsController is built without using + /// this functionality.) + /// + func databaseWillChange(with event: DatabasePreUpdateEvent) + #endif +} + +/// This class manages the observation extent of a transaction observer +private final class ManagedTransactionObserver : TransactionObserver { + let extent: Database.TransactionObservationExtent + private weak var weakObserver: TransactionObserver? + private var strongObserver: TransactionObserver? + private var observer: TransactionObserver? { return strongObserver ?? weakObserver } + + fileprivate var isObserving: Bool { + return observer != nil + } + + init(observer: TransactionObserver, extent: Database.TransactionObservationExtent) { + self.extent = extent + switch extent { + case .observerLifetime: + weakObserver = observer + case .nextTransaction: + // This strong reference will be released in databaseDidCommit() and databaseDidRollback() + strongObserver = observer + case .databaseLifetime: + strongObserver = observer + } + } + + func isWrapping(_ observer: TransactionObserver) -> Bool { + return self.observer === observer + } + + func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { + return observer?.observes(eventsOfKind: eventKind) ?? false + } + + func databaseDidChange(with event: DatabaseEvent) { + observer?.databaseDidChange(with: event) + } + + func databaseWillCommit() throws { + try observer?.databaseWillCommit() + } + + func databaseDidCommit(_ db: Database) { + switch extent { + case .observerLifetime, .databaseLifetime: + observer?.databaseDidCommit(db) + case .nextTransaction: + if let observer = self.observer { + // make sure observer is no longer notified + strongObserver = nil + observer.databaseDidCommit(db) + } + } + } + + func databaseDidRollback(_ db: Database) { + switch extent { + case .observerLifetime, .databaseLifetime: + observer?.databaseDidRollback(db) + case .nextTransaction: + if let observer = self.observer { + // make sure observer is no longer notified + strongObserver = nil + observer.databaseDidRollback(db) + } + } + } + + #if SQLITE_ENABLE_PREUPDATE_HOOK + func databaseWillChange(with event: DatabasePreUpdateEvent) { + observer?.databaseWillChange(with: event) + } + #endif +} + +/// A kind of database event. See Database.add(transactionObserver:) +/// and DatabaseWriter.add(transactionObserver:). +public enum DatabaseEventKind { + /// The insertion of a row in a database table + case insert(tableName: String) + + /// The deletion of a row in a database table + case delete(tableName: String) + + /// The update of a set of columns in a database table + case update(tableName: String, columnNames: Set<String>) + + /// Returns whether event has any impact on tables and columns described + /// by selectionInfo. + public func impacts(_ selectionInfo: SelectStatement.SelectionInfo) -> Bool { + switch self { + case .delete(let tableName): + return selectionInfo.contains(anyColumnFrom: tableName) + case .insert(let tableName): + return selectionInfo.contains(anyColumnFrom: tableName) + case .update(let tableName, let updatedColumnNames): + return selectionInfo.contains(anyColumnIn: updatedColumnNames, from: tableName) + } + } + +} + +extension DatabaseEventKind { + /// The impacted database table + public var tableName: String { + switch self { + case .insert(tableName: let tableName): return tableName + case .delete(tableName: let tableName): return tableName + case .update(tableName: let tableName, columnNames: _): return tableName + } + } +} + +protocol DatabaseEventProtocol { + func send(to observer: TransactionObserver) +} + +/// A database event, notified to TransactionObserver. +public struct DatabaseEvent { + + /// An event kind + public enum Kind: Int32 { + /// SQLITE_INSERT + case insert = 18 + + /// SQLITE_DELETE + case delete = 9 + + /// SQLITE_UPDATE + case update = 23 + } + + /// The event kind + public let kind: Kind + + /// The database name + public var databaseName: String { return impl.databaseName } + + /// The table name + public var tableName: String { return impl.tableName } + + /// The rowID of the changed row. + public let rowID: Int64 + + /// Returns an event that can be stored: + /// + /// class MyObserver: TransactionObserver { + /// var events: [DatabaseEvent] + /// func databaseDidChange(with event: DatabaseEvent) { + /// events.append(event.copy()) + /// } + /// } + public func copy() -> DatabaseEvent { + return impl.copy(self) + } + + fileprivate init(kind: Kind, rowID: Int64, impl: DatabaseEventImpl) { + self.kind = kind + self.rowID = rowID + self.impl = impl + } + + init(kind: Kind, rowID: Int64, databaseNameCString: UnsafePointer<Int8>?, tableNameCString: UnsafePointer<Int8>?) { + self.init(kind: kind, rowID: rowID, impl: MetalDatabaseEventImpl(databaseNameCString: databaseNameCString, tableNameCString: tableNameCString)) + } + + private let impl: DatabaseEventImpl +} + +extension DatabaseEvent : DatabaseEventProtocol { + func send(to observer: TransactionObserver) { + observer.databaseDidChange(with: self) + } +} + +/// Protocol for internal implementation of DatabaseEvent +private protocol DatabaseEventImpl { + var databaseName: String { get } + var tableName: String { get } + func copy(_ event: DatabaseEvent) -> DatabaseEvent +} + +/// Optimization: MetalDatabaseEventImpl does not create Swift strings from raw +/// SQLite char* until actually asked for databaseName or tableName. +private struct MetalDatabaseEventImpl : DatabaseEventImpl { + let databaseNameCString: UnsafePointer<Int8>? + let tableNameCString: UnsafePointer<Int8>? + + var databaseName: String { return String(cString: databaseNameCString!) } + var tableName: String { return String(cString: tableNameCString!) } + func copy(_ event: DatabaseEvent) -> DatabaseEvent { + return DatabaseEvent(kind: event.kind, rowID: event.rowID, impl: CopiedDatabaseEventImpl(databaseName: databaseName, tableName: tableName)) + } +} + +/// Impl for DatabaseEvent that contains copies of event strings. +private struct CopiedDatabaseEventImpl : DatabaseEventImpl { + let databaseName: String + let tableName: String + func copy(_ event: DatabaseEvent) -> DatabaseEvent { + return event + } +} + +#if SQLITE_ENABLE_PREUPDATE_HOOK + + public struct DatabasePreUpdateEvent { + + /// An event kind + public enum Kind: Int32 { + /// SQLITE_INSERT + case insert = 18 + + /// SQLITE_DELETE + case delete = 9 + + /// SQLITE_UPDATE + case update = 23 + } + + /// The event kind + public let kind: Kind + + /// The database name + public var databaseName: String { return impl.databaseName } + + /// The table name + public var tableName: String { return impl.tableName } + + /// The number of columns in the row that is being inserted, updated, or deleted. + public var count: Int { return Int(impl.columnsCount) } + + /// The triggering depth of the row update + /// Returns: + /// 0 if the preupdate callback was invoked as a result of a direct insert, + // update, or delete operation; + /// 1 for inserts, updates, or deletes invoked by top-level triggers; + /// 2 for changes resulting from triggers called by top-level triggers; + /// ... and so forth + public var depth: CInt { return impl.depth } + + /// The initial rowID of the row being changed for .Update and .Delete changes, + /// and nil for .Insert changes. + public let initialRowID: Int64? + + /// The final rowID of the row being changed for .Update and .Insert changes, + /// and nil for .Delete changes. + public let finalRowID: Int64? + + /// The initial database values in the row. + /// + /// Values appear in the same order as the columns in the table. + /// + /// The result is nil if the event is an .Insert event. + public var initialDatabaseValues: [DatabaseValue]? { + guard (kind == .update || kind == .delete) else { return nil } + return impl.initialDatabaseValues + } + + /// Returns the initial `DatabaseValue` at given index. + /// + /// Indexes span from 0 for the leftmost column to (row.count - 1) for the + /// righmost column. + /// + /// The result is nil if the event is an .Insert event. + public func initialDatabaseValue(atIndex index: Int) -> DatabaseValue? { + GRDBPrecondition(index >= 0 && index < count, "row index out of range") + guard (kind == .update || kind == .delete) else { return nil } + return impl.initialDatabaseValue(atIndex: index) + } + + /// The final database values in the row. + /// + /// Values appear in the same order as the columns in the table. + /// + /// The result is nil if the event is a .Delete event. + public var finalDatabaseValues: [DatabaseValue]? { + guard (kind == .update || kind == .insert) else { return nil } + return impl.finalDatabaseValues + } + + /// Returns the final `DatabaseValue` at given index. + /// + /// Indexes span from 0 for the leftmost column to (row.count - 1) for the + /// righmost column. + /// + /// The result is nil if the event is a .Delete event. + public func finalDatabaseValue(atIndex index: Int) -> DatabaseValue? { + GRDBPrecondition(index >= 0 && index < count, "row index out of range") + guard (kind == .update || kind == .insert) else { return nil } + return impl.finalDatabaseValue(atIndex: index) + } + + /// Returns an event that can be stored: + /// + /// class MyObserver: TransactionObserver { + /// var events: [DatabasePreUpdateEvent] + /// func databaseWillChange(with event: DatabasePreUpdateEvent) { + /// events.append(event.copy()) + /// } + /// } + public func copy() -> DatabasePreUpdateEvent { + return impl.copy(self) + } + + fileprivate init(kind: Kind, initialRowID: Int64?, finalRowID: Int64?, impl: DatabasePreUpdateEventImpl) { + self.kind = kind + self.initialRowID = (kind == .update || kind == .delete ) ? initialRowID : nil + self.finalRowID = (kind == .update || kind == .insert ) ? finalRowID : nil + self.impl = impl + } + + init(connection: SQLiteConnection, kind: Kind, initialRowID: Int64, finalRowID: Int64, databaseNameCString: UnsafePointer<Int8>?, tableNameCString: UnsafePointer<Int8>?) { + self.init(kind: kind, + initialRowID: (kind == .update || kind == .delete ) ? finalRowID : nil, + finalRowID: (kind == .update || kind == .insert ) ? finalRowID : nil, + impl: MetalDatabasePreUpdateEventImpl(connection: connection, kind: kind, databaseNameCString: databaseNameCString, tableNameCString: tableNameCString)) + } + + private let impl: DatabasePreUpdateEventImpl + } + + extension DatabasePreUpdateEvent : DatabaseEventProtocol { + func send(to observer: TransactionObserver) { + observer.databaseWillChange(with: self) + } + } + + /// Protocol for internal implementation of DatabaseEvent + private protocol DatabasePreUpdateEventImpl { + var databaseName: String { get } + var tableName: String { get } + + var columnsCount: CInt { get } + var depth: CInt { get } + var initialDatabaseValues: [DatabaseValue]? { get } + var finalDatabaseValues: [DatabaseValue]? { get } + + func initialDatabaseValue(atIndex index: Int) -> DatabaseValue? + func finalDatabaseValue(atIndex index: Int) -> DatabaseValue? + + func copy(_ event: DatabasePreUpdateEvent) -> DatabasePreUpdateEvent + } + + /// Optimization: MetalDatabasePreUpdateEventImpl does not create Swift strings from raw + /// SQLite char* until actually asked for databaseName or tableName, + /// nor does it request other data via the sqlite3_preupdate_* APIs + /// until asked. + private struct MetalDatabasePreUpdateEventImpl : DatabasePreUpdateEventImpl { + let connection: SQLiteConnection + let kind: DatabasePreUpdateEvent.Kind + + let databaseNameCString: UnsafePointer<Int8>? + let tableNameCString: UnsafePointer<Int8>? + + var databaseName: String { return String(cString: databaseNameCString!) } + var tableName: String { return String(cString: tableNameCString!) } + + var columnsCount: CInt { return sqlite3_preupdate_count(connection) } + var depth: CInt { return sqlite3_preupdate_depth(connection) } + var initialDatabaseValues: [DatabaseValue]? { + guard (kind == .update || kind == .delete) else { return nil } + return preupdate_getValues_old(connection) + } + + var finalDatabaseValues: [DatabaseValue]? { + guard (kind == .update || kind == .insert) else { return nil } + return preupdate_getValues_new(connection) + } + + func initialDatabaseValue(atIndex index: Int) -> DatabaseValue? { + let columnCount = columnsCount + precondition(index >= 0 && index < Int(columnCount), "row index out of range") + return getValue(connection, column: CInt(index), sqlite_func: { (connection: SQLiteConnection, column: CInt, value: inout SQLiteValue? ) -> CInt in + return sqlite3_preupdate_old(connection, column, &value) + }) + } + + func finalDatabaseValue(atIndex index: Int) -> DatabaseValue? { + let columnCount = columnsCount + precondition(index >= 0 && index < Int(columnCount), "row index out of range") + return getValue(connection, column: CInt(index), sqlite_func: { (connection: SQLiteConnection, column: CInt, value: inout SQLiteValue? ) -> CInt in + return sqlite3_preupdate_new(connection, column, &value) + }) + } + + func copy(_ event: DatabasePreUpdateEvent) -> DatabasePreUpdateEvent { + return DatabasePreUpdateEvent(kind: event.kind, initialRowID: event.initialRowID, finalRowID: event.finalRowID, impl: CopiedDatabasePreUpdateEventImpl( + databaseName: databaseName, + tableName: tableName, + columnsCount: columnsCount, + depth: depth, + initialDatabaseValues: initialDatabaseValues, + finalDatabaseValues: finalDatabaseValues)) + } + + private func preupdate_getValues(_ connection: SQLiteConnection, sqlite_func: (_ connection: SQLiteConnection, _ column: CInt, _ value: inout SQLiteValue? ) -> CInt ) -> [DatabaseValue]? { + let columnCount = sqlite3_preupdate_count(connection) + guard columnCount > 0 else { return nil } + + var columnValues = [DatabaseValue]() + + for i in 0..<columnCount { + let value = getValue(connection, column: i, sqlite_func: sqlite_func)! + columnValues.append(value) + } + + return columnValues + } + + private func getValue(_ connection: SQLiteConnection, column: CInt, sqlite_func: (_ connection: SQLiteConnection, _ column: CInt, _ value: inout SQLiteValue? ) -> CInt ) -> DatabaseValue? { + var value : SQLiteValue? = nil + guard sqlite_func(connection, column, &value) == SQLITE_OK else { return nil } + if let value = value { + return DatabaseValue(sqliteValue: value) + } + return nil + } + + private func preupdate_getValues_old(_ connection: SQLiteConnection) -> [DatabaseValue]? { + return preupdate_getValues(connection, sqlite_func: { (connection: SQLiteConnection, column: CInt, value: inout SQLiteValue? ) -> CInt in + return sqlite3_preupdate_old(connection, column, &value) + }) + } + + private func preupdate_getValues_new(_ connection: SQLiteConnection) -> [DatabaseValue]? { + return preupdate_getValues(connection, sqlite_func: { (connection: SQLiteConnection, column: CInt, value: inout SQLiteValue? ) -> CInt in + return sqlite3_preupdate_new(connection, column, &value) + }) + } + } + + /// Impl for DatabasePreUpdateEvent that contains copies of all event data. + private struct CopiedDatabasePreUpdateEventImpl : DatabasePreUpdateEventImpl { + let databaseName: String + let tableName: String + let columnsCount: CInt + let depth: CInt + let initialDatabaseValues: [DatabaseValue]? + let finalDatabaseValues: [DatabaseValue]? + + func initialDatabaseValue(atIndex index: Int) -> DatabaseValue? { return initialDatabaseValues?[index] } + func finalDatabaseValue(atIndex index: Int) -> DatabaseValue? { return finalDatabaseValues?[index] } + + func copy(_ event: DatabasePreUpdateEvent) -> DatabasePreUpdateEvent { + return event + } + } + +#endif + +/// The SQLite savepoint stack is described at +/// https://www.sqlite.org/lang_savepoint.html +/// +/// This class reimplements the SQLite stack, so that we can: +/// +/// - know if there are currently active savepoints (isEmpty) +/// - buffer database events when a savepoint is active, in order to avoid +/// notifying transaction observers of database events that could be +/// rollbacked. +class SavepointStack { + /// The buffered events. See Database.didChange(with:) + fileprivate var eventsBuffer: [(event: DatabaseEventProtocol, observers: [TransactionObserver])] = [] + + /// The savepoint stack, as an array of tuples (savepointName, index in the eventsBuffer array). + /// Indexes let us drop rollbacked events from the event buffer. + private var savepoints: [(name: String, index: Int)] = [] + + var isEmpty: Bool { return savepoints.isEmpty } + + func clear() { + eventsBuffer.removeAll() + savepoints.removeAll() + } + + func beginSavepoint(named name: String) { + savepoints.append((name: name.lowercased(), index: eventsBuffer.count)) + } + + // https://www.sqlite.org/lang_savepoint.html + // > The ROLLBACK command with a TO clause rolls back transactions going + // > backwards in time back to the most recent SAVEPOINT with a matching + // > name. The SAVEPOINT with the matching name remains on the transaction + // > stack, but all database changes that occurred after that SAVEPOINT was + // > created are rolled back. If the savepoint-name in a ROLLBACK TO + // > command does not match any SAVEPOINT on the stack, then the ROLLBACK + // > command fails with an error and leaves the state of the + // > database unchanged. + func rollbackSavepoint(named name: String) { + let name = name.lowercased() + while let pair = savepoints.last, pair.name != name { + savepoints.removeLast() + } + if let savepoint = savepoints.last { + eventsBuffer.removeLast(eventsBuffer.count - savepoint.index) + } + assert(!savepoints.isEmpty || eventsBuffer.isEmpty) + } + + // https://www.sqlite.org/lang_savepoint.html + // > The RELEASE command starts with the most recent addition to the + // > transaction stack and releases savepoints backwards in time until it + // > releases a savepoint with a matching savepoint-name. Prior savepoints, + // > even savepoints with matching savepoint-names, are unchanged. + func releaseSavepoint(named name: String) { + let name = name.lowercased() + while let pair = savepoints.last, pair.name != name { + savepoints.removeLast() + } + if !savepoints.isEmpty { + savepoints.removeLast() + } + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/DatabaseError.swift b/Pods/GRDB.swift/GRDB/Core/DatabaseError.swift new file mode 100644 index 0000000..24a0be2 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/DatabaseError.swift @@ -0,0 +1,256 @@ +import Foundation +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif + +public struct ResultCode : RawRepresentable, Equatable, CustomStringConvertible { + public let rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + /// A result code limited to the least significant 8 bits of the receiver. + /// See https://www.sqlite.org/rescode.html for more information. + /// + /// let resultCode = .SQLITE_CONSTRAINT_FOREIGNKEY + /// resultCode.primaryResultCode == .SQLITE_CONSTRAINT // true + public var primaryResultCode: ResultCode { + return ResultCode(rawValue: rawValue & 0xFF) + } + + var isPrimary: Bool { + return self == primaryResultCode + } + + public var description: String { + // sqlite3_errstr was added in SQLite 3.7.15 http://www.sqlite.org/changes.html#version_3_7_15 + // It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS) + #if GRDBCUSTOMSQLITE || GRDBCIPHER + return "\(rawValue) (\(String(cString: sqlite3_errstr(rawValue))))" + #else + if #available(iOS 8.2, OSX 10.10, OSXApplicationExtension 10.10, iOSApplicationExtension 8.2, *) { + return "\(rawValue) (\(String(cString: sqlite3_errstr(rawValue))))" + } else { + return "\(rawValue)" + } + #endif + } + + public static func == (_ lhs: ResultCode, _ rhs: ResultCode) -> Bool { + return lhs.rawValue == rhs.rawValue + } + + /// Returns true if the code on the left matches the code on the right. + /// + /// Primary result codes match themselves and their extended result codes, + /// while extended result codes match only themselves: + /// + /// switch error.extendedResultCode { + /// case .SQLITE_CONSTRAINT_FOREIGNKEY: // foreign key constraint error + /// case .SQLITE_CONSTRAINT: // any other constraint error + /// default: // any other database error + /// } + public static func ~= (pattern: ResultCode, code: ResultCode) -> Bool { + if pattern.isPrimary { + return pattern == code.primaryResultCode + } else { + return pattern == code + } + } + + // Primary Result codes + // https://www.sqlite.org/rescode.html#primary_result_code_list + + public static let SQLITE_OK = ResultCode(rawValue: 0) // Successful result + public static let SQLITE_ERROR = ResultCode(rawValue: 1) // SQL error or missing database + public static let SQLITE_INTERNAL = ResultCode(rawValue: 2) // Internal logic error in SQLite + public static let SQLITE_PERM = ResultCode(rawValue: 3) // Access permission denied + public static let SQLITE_ABORT = ResultCode(rawValue: 4) // Callback routine requested an abort + public static let SQLITE_BUSY = ResultCode(rawValue: 5) // The database file is locked + public static let SQLITE_LOCKED = ResultCode(rawValue: 6) // A table in the database is locked + public static let SQLITE_NOMEM = ResultCode(rawValue: 7) // A malloc() failed + public static let SQLITE_READONLY = ResultCode(rawValue: 8) // Attempt to write a readonly database + public static let SQLITE_INTERRUPT = ResultCode(rawValue: 9) // Operation terminated by sqlite3_interrupt() + public static let SQLITE_IOERR = ResultCode(rawValue: 10) // Some kind of disk I/O error occurred + public static let SQLITE_CORRUPT = ResultCode(rawValue: 11) // The database disk image is malformed + public static let SQLITE_NOTFOUND = ResultCode(rawValue: 12) // Unknown opcode in sqlite3_file_control() + public static let SQLITE_FULL = ResultCode(rawValue: 13) // Insertion failed because database is full + public static let SQLITE_CANTOPEN = ResultCode(rawValue: 14) // Unable to open the database file + public static let SQLITE_PROTOCOL = ResultCode(rawValue: 15) // Database lock protocol error + public static let SQLITE_EMPTY = ResultCode(rawValue: 16) // Database is empty + public static let SQLITE_SCHEMA = ResultCode(rawValue: 17) // The database schema changed + public static let SQLITE_TOOBIG = ResultCode(rawValue: 18) // String or BLOB exceeds size limit + public static let SQLITE_CONSTRAINT = ResultCode(rawValue: 19) // Abort due to constraint violation + public static let SQLITE_MISMATCH = ResultCode(rawValue: 20) // Data type mismatch + public static let SQLITE_MISUSE = ResultCode(rawValue: 21) // Library used incorrectly + public static let SQLITE_NOLFS = ResultCode(rawValue: 22) // Uses OS features not supported on host + public static let SQLITE_AUTH = ResultCode(rawValue: 23) // Authorization denied + public static let SQLITE_FORMAT = ResultCode(rawValue: 24) // Auxiliary database format error + public static let SQLITE_RANGE = ResultCode(rawValue: 25) // 2nd parameter to sqlite3_bind out of range + public static let SQLITE_NOTADB = ResultCode(rawValue: 26) // File opened that is not a database file + public static let SQLITE_NOTICE = ResultCode(rawValue: 27) // Notifications from sqlite3_log() + public static let SQLITE_WARNING = ResultCode(rawValue: 28) // Warnings from sqlite3_log() + public static let SQLITE_ROW = ResultCode(rawValue: 100) // sqlite3_step() has another row ready + public static let SQLITE_DONE = ResultCode(rawValue: 101) // sqlite3_step() has finished executing + + // Extended Result Code + // https://www.sqlite.org/rescode.html#extended_result_code_list + + public static let SQLITE_IOERR_READ = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (1<<8))) + public static let SQLITE_IOERR_SHORT_READ = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (2<<8))) + public static let SQLITE_IOERR_WRITE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (3<<8))) + public static let SQLITE_IOERR_FSYNC = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (4<<8))) + public static let SQLITE_IOERR_DIR_FSYNC = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (5<<8))) + public static let SQLITE_IOERR_TRUNCATE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (6<<8))) + public static let SQLITE_IOERR_FSTAT = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (7<<8))) + public static let SQLITE_IOERR_UNLOCK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (8<<8))) + public static let SQLITE_IOERR_RDLOCK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (9<<8))) + public static let SQLITE_IOERR_DELETE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (10<<8))) + public static let SQLITE_IOERR_BLOCKED = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (11<<8))) + public static let SQLITE_IOERR_NOMEM = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (12<<8))) + public static let SQLITE_IOERR_ACCESS = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (13<<8))) + public static let SQLITE_IOERR_CHECKRESERVEDLOCK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (14<<8))) + public static let SQLITE_IOERR_LOCK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (15<<8))) + public static let SQLITE_IOERR_CLOSE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (16<<8))) + public static let SQLITE_IOERR_DIR_CLOSE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (17<<8))) + public static let SQLITE_IOERR_SHMOPEN = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (18<<8))) + public static let SQLITE_IOERR_SHMSIZE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (19<<8))) + public static let SQLITE_IOERR_SHMLOCK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (20<<8))) + public static let SQLITE_IOERR_SHMMAP = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (21<<8))) + public static let SQLITE_IOERR_SEEK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (22<<8))) + public static let SQLITE_IOERR_DELETE_NOENT = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (23<<8))) + public static let SQLITE_IOERR_MMAP = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (24<<8))) + public static let SQLITE_IOERR_GETTEMPPATH = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (25<<8))) + public static let SQLITE_IOERR_CONVPATH = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (26<<8))) + public static let SQLITE_IOERR_VNODE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (27<<8))) + public static let SQLITE_IOERR_AUTH = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (28<<8))) + public static let SQLITE_LOCKED_SHAREDCACHE = ResultCode(rawValue: (SQLITE_LOCKED.rawValue | (1<<8))) + public static let SQLITE_BUSY_RECOVERY = ResultCode(rawValue: (SQLITE_BUSY.rawValue | (1<<8))) + public static let SQLITE_BUSY_SNAPSHOT = ResultCode(rawValue: (SQLITE_BUSY.rawValue | (2<<8))) + public static let SQLITE_CANTOPEN_NOTEMPDIR = ResultCode(rawValue: (SQLITE_CANTOPEN.rawValue | (1<<8))) + public static let SQLITE_CANTOPEN_ISDIR = ResultCode(rawValue: (SQLITE_CANTOPEN.rawValue | (2<<8))) + public static let SQLITE_CANTOPEN_FULLPATH = ResultCode(rawValue: (SQLITE_CANTOPEN.rawValue | (3<<8))) + public static let SQLITE_CANTOPEN_CONVPATH = ResultCode(rawValue: (SQLITE_CANTOPEN.rawValue | (4<<8))) + public static let SQLITE_CORRUPT_VTAB = ResultCode(rawValue: (SQLITE_CORRUPT.rawValue | (1<<8))) + public static let SQLITE_READONLY_RECOVERY = ResultCode(rawValue: (SQLITE_READONLY.rawValue | (1<<8))) + public static let SQLITE_READONLY_CANTLOCK = ResultCode(rawValue: (SQLITE_READONLY.rawValue | (2<<8))) + public static let SQLITE_READONLY_ROLLBACK = ResultCode(rawValue: (SQLITE_READONLY.rawValue | (3<<8))) + public static let SQLITE_READONLY_DBMOVED = ResultCode(rawValue: (SQLITE_READONLY.rawValue | (4<<8))) + public static let SQLITE_ABORT_ROLLBACK = ResultCode(rawValue: (SQLITE_ABORT.rawValue | (2<<8))) + public static let SQLITE_CONSTRAINT_CHECK = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (1<<8))) + public static let SQLITE_CONSTRAINT_COMMITHOOK = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (2<<8))) + public static let SQLITE_CONSTRAINT_FOREIGNKEY = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (3<<8))) + public static let SQLITE_CONSTRAINT_FUNCTION = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (4<<8))) + public static let SQLITE_CONSTRAINT_NOTNULL = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (5<<8))) + public static let SQLITE_CONSTRAINT_PRIMARYKEY = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (6<<8))) + public static let SQLITE_CONSTRAINT_TRIGGER = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (7<<8))) + public static let SQLITE_CONSTRAINT_UNIQUE = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (8<<8))) + public static let SQLITE_CONSTRAINT_VTAB = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (9<<8))) + public static let SQLITE_CONSTRAINT_ROWID = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (10<<8))) + public static let SQLITE_NOTICE_RECOVER_WAL = ResultCode(rawValue: (SQLITE_NOTICE.rawValue | (1<<8))) + public static let SQLITE_NOTICE_RECOVER_ROLLBACK = ResultCode(rawValue: (SQLITE_NOTICE.rawValue | (2<<8))) + public static let SQLITE_WARNING_AUTOINDEX = ResultCode(rawValue: (SQLITE_WARNING.rawValue | (1<<8))) + public static let SQLITE_AUTH_USER = ResultCode(rawValue: (SQLITE_AUTH.rawValue | (1<<8))) + public static let SQLITE_OK_LOAD_PERMANENTLY = ResultCode(rawValue: (SQLITE_OK.rawValue | (1<<8))) + +} + +/// DatabaseError wraps an SQLite error. +public struct DatabaseError : Error { + + /// The SQLite error code (see + /// https://www.sqlite.org/rescode.html#primary_result_code_list). + /// + /// do { + /// ... + /// } catch let error as DatabaseError where error.resultCode == .SQL_CONSTRAINT { + /// // A constraint error + /// } + /// + /// This property returns a "primary result code", that is to say the least + /// significant 8 bits of any SQLite result code. See + /// https://www.sqlite.org/rescode.html for more information. + /// + /// See also `extendedResultCode`. + public var resultCode: ResultCode { + return extendedResultCode.primaryResultCode + } + + /// The SQLite extended error code (see + /// https://www.sqlite.org/rescode.html#extended_result_code_list). + /// + /// do { + /// ... + /// } catch let error as DatabaseError where error.extendedResultCode == .SQLITE_CONSTRAINT_FOREIGNKEY { + /// // A foreign key constraint error + /// } + /// + /// See also `resultCode`. + public let extendedResultCode: ResultCode + + /// The SQLite error message. + public let message: String? + + /// The SQL query that yielded the error (if relevant). + public let sql: String? + + /// Creates a Database Error + public init(resultCode: ResultCode = .SQLITE_ERROR, message: String? = nil, sql: String? = nil, arguments: StatementArguments? = nil) { + self.extendedResultCode = resultCode + self.message = message + self.sql = sql + self.arguments = arguments + } + + /// Creates a Database Error with a raw Int32 result code. + /// + /// This initializer is not public because library user is not supposed to + /// be exposed to raw result codes. + init(resultCode: Int32, message: String? = nil, sql: String? = nil, arguments: StatementArguments? = nil) { + self.init(resultCode: ResultCode(rawValue: resultCode), message: message, sql: sql, arguments: arguments) + } + + // MARK: Not public + + /// The query arguments that yielded the error (if relevant). + /// Not public because the StatementArguments class has no public method. + let arguments: StatementArguments? +} + +extension DatabaseError: CustomStringConvertible { + /// A textual representation of `self`. + public var description: String { + var description = "SQLite error \(resultCode.rawValue)" + if let sql = sql { + description += " with statement `\(sql)`" + } + if let arguments = arguments, !arguments.isEmpty { + description += " arguments \(arguments)" + } + if let message = message { + description += ": \(message)" + } + return description + } +} + +extension DatabaseError : CustomNSError { + + /// NSError bridging: the domain of the error. + public static var errorDomain: String { + return "GRDB.DatabaseError" + } + + /// NSError bridging: the error code within the given domain. + public var errorCode: Int { + return Int(extendedResultCode.rawValue) + } + + /// NSError bridging: the user-info dictionary. + public var errorUserInfo: [String : Any] { + return [NSLocalizedDescriptionKey: description] + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/DatabaseFunction.swift b/Pods/GRDB.swift/GRDB/Core/DatabaseFunction.swift new file mode 100644 index 0000000..cecc315 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/DatabaseFunction.swift @@ -0,0 +1,371 @@ +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif + +/// An SQL function or aggregate. +public final class DatabaseFunction { + public let name: String + let argumentCount: Int32? + let pure: Bool + private let kind: Kind + private var nArg: Int32 { return argumentCount ?? -1 } + private var eTextRep: Int32 { return (SQLITE_UTF8 | (pure ? SQLITE_DETERMINISTIC : 0)) } + + /// Returns an SQL function. + /// + /// let fn = DatabaseFunction("succ", argumentCount: 1) { dbValues in + /// guard let int = Int.fromDatabaseValue(dbValues[0]) else { + /// return nil + /// } + /// return int + 1 + /// } + /// db.add(function: fn) + /// try Int.fetchOne(db, "SELECT succ(1)")! // 2 + /// + /// - parameters: + /// - name: The function name. + /// - argumentCount: The number of arguments of the function. If + /// omitted, or nil, the function accepts any number of arguments. + /// - pure: Whether the function is "pure", which means that its results + /// only depends on its inputs. When a function is pure, SQLite has + /// the opportunity to perform additional optimizations. Default value + /// is false. + /// - function: A function that takes an array of DatabaseValue + /// arguments, and returns an optional DatabaseValueConvertible such + /// as Int, String, NSDate, etc. The array is guaranteed to have + /// exactly *argumentCount* elements, provided *argumentCount* is + /// not nil. + public init(_ name: String, argumentCount: Int32? = nil, pure: Bool = false, function: @escaping ([DatabaseValue]) throws -> DatabaseValueConvertible?) { + self.name = name + self.argumentCount = argumentCount + self.pure = pure + self.kind = .function{ (argc, argv) in + let arguments = (0..<Int(argc)).map { index in + DatabaseValue(sqliteValue: argv.unsafelyUnwrapped[index]!) + } + return try function(arguments) + } + } + + /// Returns an SQL aggregate function. + /// + /// struct MySum : DatabaseAggregate { + /// var sum: Int = 0 + /// + /// mutating func step(_ dbValues: [DatabaseValue]) { + /// if let int = Int.fromDatabaseValue(dbValues[0]) { + /// sum += int + /// } + /// } + /// + /// func finalize() -> DatabaseValueConvertible? { + /// return sum + /// } + /// } + /// + /// let dbQueue = DatabaseQueue() + /// let fn = DatabaseFunction("mysum", argumentCount: 1, aggregate: MySum.self) + /// dbQueue.add(function: fn) + /// try dbQueue.inDatabase { db in + /// try db.execute("CREATE TABLE test(i)") + /// try db.execute("INSERT INTO test(i) VALUES (1)") + /// try db.execute("INSERT INTO test(i) VALUES (2)") + /// try Int.fetchOne(db, "SELECT mysum(i) FROM test")! // 3 + /// } + /// + /// - parameters: + /// - name: The function name. + /// - argumentCount: The number of arguments of the aggregate. If + /// omitted, or nil, the aggregate accepts any number of arguments. + /// - pure: Whether the aggregate is "pure", which means that its + /// results only depends on its inputs. When an aggregate is pure, + /// SQLite has the opportunity to perform additional optimizations. + /// Default value is false. + /// - aggregate: A type that implements the DatabaseAggregate protocol. + /// For each step of the aggregation, its `step` method is called with + /// an array of DatabaseValue arguments. The array is guaranteed to + /// have exactly *argumentCount* elements, provided *argumentCount* is + /// not nil. + public init<Aggregate: DatabaseAggregate>(_ name: String, argumentCount: Int32? = nil, pure: Bool = false, aggregate: Aggregate.Type) { + self.name = name + self.argumentCount = argumentCount + self.pure = pure + self.kind = .aggregate { return Aggregate() } + } + + /// Calls sqlite3_create_function_v2 + /// See https://sqlite.org/c3ref/create_function.html + func install(in db: Database) { + // Retain the function definition + let definition = kind.definition + let definitionP = Unmanaged.passRetained(definition).toOpaque() + + let code = sqlite3_create_function_v2( + db.sqliteConnection, + name, + nArg, + eTextRep, + definitionP, + kind.xFunc, + kind.xStep, + kind.xFinal, + { definitionP in + // Release the function definition + Unmanaged<AnyObject>.fromOpaque(definitionP!).release() + }) + + guard code == SQLITE_OK else { + // Assume a GRDB bug: there is no point throwing any error. + fatalError(DatabaseError(resultCode: code, message: db.lastErrorMessage).description) + } + } + + /// Calls sqlite3_create_function_v2 + /// See https://sqlite.org/c3ref/create_function.html + func uninstall(in db: Database) { + let code = sqlite3_create_function_v2( + db.sqliteConnection, + name, + nArg, + eTextRep, + nil, nil, nil, nil, nil) + + guard code == SQLITE_OK else { + // Assume a GRDB bug: there is no point throwing any error. + fatalError(DatabaseError(resultCode: code, message: db.lastErrorMessage).description) + } + } + + /// The way to compute the result of a function. + /// Feeds the `pApp` parameter of sqlite3_create_function_v2 + /// http://sqlite.org/capi3ref.html#sqlite3_create_function + private class FunctionDefinition { + let compute: (Int32, UnsafeMutablePointer<OpaquePointer?>?) throws -> DatabaseValueConvertible? + init(compute: @escaping (Int32, UnsafeMutablePointer<OpaquePointer?>?) throws -> DatabaseValueConvertible?) { + self.compute = compute + } + } + + /// The way to start an aggregate. + /// Feeds the `pApp` parameter of sqlite3_create_function_v2 + /// http://sqlite.org/capi3ref.html#sqlite3_create_function + private class AggregateDefinition { + let makeAggregate: () -> DatabaseAggregate + init(makeAggregate: @escaping () -> DatabaseAggregate) { + self.makeAggregate = makeAggregate + } + } + + /// The current state of an aggregate, storable in SQLite + private class AggregateContext { + var aggregate: DatabaseAggregate + var hasErrored = false + init(aggregate: DatabaseAggregate) { + self.aggregate = aggregate + } + } + + /// A function kind: an "SQL function" or an "aggregate". + /// See http://sqlite.org/capi3ref.html#sqlite3_create_function + private enum Kind { + /// A regular function: SELECT f(1) + case function((Int32, UnsafeMutablePointer<OpaquePointer?>?) throws -> DatabaseValueConvertible?) + + /// An aggregate: SELECT f(foo) FROM bar GROUP BY baz + case aggregate(() -> DatabaseAggregate) + + /// Feeds the `pApp` parameter of sqlite3_create_function_v2 + /// http://sqlite.org/capi3ref.html#sqlite3_create_function + var definition: AnyObject { + switch self { + case .function(let compute): + return FunctionDefinition(compute: compute) + case .aggregate(let makeAggregate): + return AggregateDefinition(makeAggregate: makeAggregate) + } + } + + /// Feeds the `xFunc` parameter of sqlite3_create_function_v2 + /// http://sqlite.org/capi3ref.html#sqlite3_create_function + var xFunc: (@convention(c) (OpaquePointer?, Int32, UnsafeMutablePointer<OpaquePointer?>?) -> Void)? { + guard case .function = self else { return nil } + return { (sqliteContext, argc, argv) in + let definition = Unmanaged<FunctionDefinition>.fromOpaque(sqlite3_user_data(sqliteContext)).takeUnretainedValue() + do { + try DatabaseFunction.report( + result: definition.compute(argc, argv), + in: sqliteContext) + } catch { + DatabaseFunction.report(error: error, in: sqliteContext) + } + } + } + + /// Feeds the `xStep` parameter of sqlite3_create_function_v2 + /// http://sqlite.org/capi3ref.html#sqlite3_create_function + var xStep: (@convention(c) (OpaquePointer?, Int32, UnsafeMutablePointer<OpaquePointer?>?) -> Void)? { + guard case .aggregate = self else { return nil } + return { (sqliteContext, argc, argv) in + let aggregateContextU = DatabaseFunction.unmanagedAggregateContext(sqliteContext) + let aggregateContext = aggregateContextU.takeUnretainedValue() + assert(!aggregateContext.hasErrored) + do { + let arguments = (0..<Int(argc)).map { index in + DatabaseValue(sqliteValue: argv.unsafelyUnwrapped[index]!) + } + try aggregateContext.aggregate.step(arguments) + } catch { + aggregateContext.hasErrored = true + DatabaseFunction.report(error: error, in: sqliteContext) + } + } + } + + /// Feeds the `xFinal` parameter of sqlite3_create_function_v2 + /// http://sqlite.org/capi3ref.html#sqlite3_create_function + var xFinal: (@convention(c) (OpaquePointer?) -> Void)? { + guard case .aggregate = self else { return nil } + return { (sqliteContext) in + let aggregateContextU = DatabaseFunction.unmanagedAggregateContext(sqliteContext) + let aggregateContext = aggregateContextU.takeUnretainedValue() + aggregateContextU.release() + + guard !aggregateContext.hasErrored else { + return + } + + do { + try DatabaseFunction.report( + result: aggregateContext.aggregate.finalize(), + in: sqliteContext) + } catch { + DatabaseFunction.report(error: error, in: sqliteContext) + } + } + } + } + + /// Helper function that extracts the current state of an aggregate from an + /// sqlite function execution context. + /// + /// The result must be released when the aggregate concludes. + /// + /// See https://sqlite.org/c3ref/context.html + /// See https://sqlite.org/c3ref/aggregate_context.html + private static func unmanagedAggregateContext(_ sqliteContext: OpaquePointer?) -> Unmanaged<AggregateContext> { + // The current aggregate buffer + let stride = MemoryLayout<Unmanaged<AggregateContext>>.stride + let aggregateContextBufferP = UnsafeMutableRawBufferPointer(start: sqlite3_aggregate_context(sqliteContext, Int32(stride))!, count: stride) + + if aggregateContextBufferP.contains(where: { $0 != 0 }) { + // Buffer contains non-null pointer: load aggregate context + let aggregateContextP = aggregateContextBufferP.baseAddress!.assumingMemoryBound(to: Unmanaged<AggregateContext>.self) + return aggregateContextP.pointee + } else { + // Buffer contains null pointer: create aggregate context... + let aggregate = Unmanaged<AggregateDefinition>.fromOpaque(sqlite3_user_data(sqliteContext)) + .takeUnretainedValue() + .makeAggregate() + let aggregateContext = AggregateContext(aggregate: aggregate) + + // retain and store in SQLite's buffer + let aggregateContextU = Unmanaged.passRetained(aggregateContext) + var aggregateContextP = aggregateContextU.toOpaque() + withUnsafeBytes(of: &aggregateContextP) { + aggregateContextBufferP.copyBytes(from: $0) + } + return aggregateContextU + } + } + + private static func report(result: DatabaseValueConvertible?, in sqliteContext: OpaquePointer?) { + switch result?.databaseValue.storage ?? .null { + case .null: + sqlite3_result_null(sqliteContext) + case .int64(let int64): + sqlite3_result_int64(sqliteContext, int64) + case .double(let double): + sqlite3_result_double(sqliteContext, double) + case .string(let string): + sqlite3_result_text(sqliteContext, string, -1, SQLITE_TRANSIENT) + case .blob(let data): + data.withUnsafeBytes { bytes in + sqlite3_result_blob(sqliteContext, bytes, Int32(data.count), SQLITE_TRANSIENT) + } + } + } + + private static func report(error: Error, in sqliteContext: OpaquePointer?) { + if let error = error as? DatabaseError { + if let message = error.message { + sqlite3_result_error(sqliteContext, message, -1) + } + sqlite3_result_error_code(sqliteContext, error.extendedResultCode.rawValue) + } else { + sqlite3_result_error(sqliteContext, "\(error)", -1) + } + } +} + +extension DatabaseFunction : Hashable { + /// The hash value + public var hashValue: Int { + return name.hashValue ^ nArg.hashValue + } + + /// Two functions are equal if they share the same name and arity. + public static func == (lhs: DatabaseFunction, rhs: DatabaseFunction) -> Bool { + return lhs.name == rhs.name && lhs.nArg == rhs.nArg + } +} + +/// The protocol for custom SQLite aggregates. +/// +/// For example: +/// +/// struct MySum : DatabaseAggregate { +/// var sum: Int = 0 +/// +/// mutating func step(_ dbValues: [DatabaseValue]) { +/// if let int = Int.fromDatabaseValue(dbValues[0]) { +/// sum += int +/// } +/// } +/// +/// func finalize() -> DatabaseValueConvertible? { +/// return sum +/// } +/// } +/// +/// let dbQueue = DatabaseQueue() +/// let fn = DatabaseFunction("mysum", argumentCount: 1, aggregate: MySum.self) +/// dbQueue.add(function: fn) +/// try dbQueue.inDatabase { db in +/// try db.execute("CREATE TABLE test(i)") +/// try db.execute("INSERT INTO test(i) VALUES (1)") +/// try db.execute("INSERT INTO test(i) VALUES (2)") +/// try Int.fetchOne(db, "SELECT mysum(i) FROM test")! // 3 +/// } +public protocol DatabaseAggregate { + /// Creates an aggregate. + init() + + /// This method is called at each step of the aggregation. + /// + /// The dbValues argument contains as many values as given to the SQL + /// aggregate function. + /// + /// -- One value + /// SELECT maxLength(name) FROM players + /// + /// -- Two values + /// SELECT maxFullNameLength(firstName, lastName) FROM players + /// + /// This method is never called after the finalize() method has been called. + mutating func step(_ dbValues: [DatabaseValue]) throws + + /// Returns the final result + func finalize() throws -> DatabaseValueConvertible? +} diff --git a/Pods/GRDB.swift/GRDB/Core/DatabasePool.swift b/Pods/GRDB.swift/GRDB/Core/DatabasePool.swift new file mode 100644 index 0000000..08115b8 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/DatabasePool.swift @@ -0,0 +1,615 @@ +import Foundation +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif +#if os(iOS) + import UIKit +#endif + +/// A DatabasePool grants concurrent accesses to an SQLite database. +public final class DatabasePool { + + // MARK: - Initializers + + /// Opens the SQLite database at path *path*. + /// + /// let dbPool = try DatabasePool(path: "/path/to/database.sqlite") + /// + /// Database connections get closed when the database pool gets deallocated. + /// + /// - parameters: + /// - path: The path to the database file. + /// - configuration: A configuration. + /// - maximumReaderCount: The maximum number of readers. Default is 5. + /// - throws: A DatabaseError whenever an SQLite error occurs. + public init(path: String, configuration: Configuration = Configuration()) throws { + GRDBPrecondition(configuration.maximumReaderCount > 0, "configuration.maximumReaderCount must be at least 1") + + // Writer and readers share the same database schema cache + let sharedSchemaCache = SharedDatabaseSchemaCache() + + // Writer + writer = try SerializedDatabase( + path: path, + configuration: configuration, + schemaCache: sharedSchemaCache) + + // Activate WAL Mode unless readonly + if !configuration.readonly { + try writer.sync { db in + let journalMode = try String.fetchOne(db, "PRAGMA journal_mode = WAL") + guard journalMode == "wal" else { + throw DatabaseError(message: "could not activate WAL Mode at path: \(path)") + } + + // https://www.sqlite.org/pragma.html#pragma_synchronous + // > Many applications choose NORMAL when in WAL mode + try db.execute("PRAGMA synchronous = NORMAL") + + if !FileManager.default.fileExists(atPath: path + "-wal") { + // Create the -wal file if it does not exist yet. This + // avoids an SQLITE_CANTOPEN (14) error whenever a user + // opens a pool to an existing non-WAL database, and + // attempts to read from it. + // See https://github.com/groue/GRDB.swift/issues/102 + try db.execute("CREATE TABLE grdb_issue_102 (id INTEGER PRIMARY KEY); DROP TABLE grdb_issue_102;") + } + } + } + + // Readers + readerConfig = configuration + readerConfig.readonly = true + readerConfig.defaultTransactionKind = .deferred // Make it the default for readers. Other transaction kinds are forbidden by SQLite in read-only connections. + readerPool = Pool(maximumCount: configuration.maximumReaderCount, makeElement: { [unowned self] in + let reader = try SerializedDatabase( + path: path, + configuration: self.readerConfig, + schemaCache: sharedSchemaCache) + + reader.sync { db in + for function in self.functions { + db.add(function: function) + } + for collation in self.collations { + db.add(collation: collation) + } + } + + return reader + }) + } + + #if os(iOS) + deinit { + // Undo job done in setupMemoryManagement() + // + // https://developer.apple.com/library/mac/releasenotes/Foundation/RN-Foundation/index.html#10_11Error + // Explicit unregistration is required before iOS 9 and OS X 10.11. + NotificationCenter.default.removeObserver(self) + } + #endif + + + // MARK: - Configuration + + /// The path to the database. + public var path: String { + return writer.path + } + + + // MARK: - WAL Management + + /// Runs a WAL checkpoint + /// + /// See https://www.sqlite.org/wal.html and + /// https://www.sqlite.org/c3ref/wal_checkpoint_v2.html) for + /// more information. + /// + /// - parameter kind: The checkpoint mode (default passive) + public func checkpoint(_ kind: Database.CheckpointMode = .passive) throws { + try write { db in + // TODO: read https://www.sqlite.org/c3ref/wal_checkpoint_v2.html and + // check whether we need a busy handler on writer and/or readers + // when kind is not .Passive. + let code = sqlite3_wal_checkpoint_v2(db.sqliteConnection, nil, kind.rawValue, nil, nil) + guard code == SQLITE_OK else { + throw DatabaseError(resultCode: code, message: db.lastErrorMessage, sql: nil) + } + } + } + + + // MARK: - Memory management + + /// Free as much memory as possible. + /// + /// This method blocks the current thread until all database accesses are completed. + /// + /// See also setupMemoryManagement(application:) + public func releaseMemory() { + // TODO: test that this method blocks the current thread until all database accesses are completed. + write { $0.releaseMemory() } + readerPool.forEach { reader in + reader.sync { $0.releaseMemory() } + } + readerPool.clear() + } + + + #if os(iOS) + /// Listens to UIApplicationDidEnterBackgroundNotification and + /// UIApplicationDidReceiveMemoryWarningNotification in order to release + /// as much memory as possible. + /// + /// - param application: The UIApplication that will start a background + /// task to let the database pool release its memory when the application + /// enters background. + public func setupMemoryManagement(in application: UIApplication) { + self.application = application + let center = NotificationCenter.default + center.addObserver(self, selector: #selector(DatabasePool.applicationDidReceiveMemoryWarning(_:)), name: .UIApplicationDidReceiveMemoryWarning, object: nil) + center.addObserver(self, selector: #selector(DatabasePool.applicationDidEnterBackground(_:)), name: .UIApplicationDidEnterBackground, object: nil) + } + + private var application: UIApplication! + + @objc private func applicationDidEnterBackground(_ notification: NSNotification) { + guard let application = application else { + return + } + + var task: UIBackgroundTaskIdentifier! = nil + task = application.beginBackgroundTask(expirationHandler: nil) + + if task == UIBackgroundTaskInvalid { + // Perform releaseMemory() synchronously. + releaseMemory() + } else { + // Perform releaseMemory() asynchronously. + DispatchQueue.global().async { + self.releaseMemory() + application.endBackgroundTask(task) + } + } + } + + @objc private func applicationDidReceiveMemoryWarning(_ notification: NSNotification) { + DispatchQueue.global().async { + self.releaseMemory() + } + } + #endif + + + // MARK: - Not public + + private let writer: SerializedDatabase + private var readerConfig: Configuration + private var readerPool: Pool<SerializedDatabase>! = nil // var and nil-initialized so that we can use `self` when creating readerPool in DatabasePool.init() + + private var functions = Set<DatabaseFunction>() + private var collations = Set<DatabaseCollation>() +} + + +// ========================================================================= +// MARK: - Encryption + +#if SQLITE_HAS_CODEC + extension DatabasePool { + + /// Changes the passphrase of an encrypted database + public func change(passphrase: String) throws { + try readerPool.clear(andThen: { + try write { try $0.change(passphrase: passphrase) } + readerConfig.passphrase = passphrase + }) + } + } +#endif + + +// ========================================================================= +// MARK: - DatabaseReader + +extension DatabasePool : DatabaseReader { + + // MARK: - Read From Database + + /// Synchronously executes a read-only block in a protected dispatch queue, + /// and returns its result. The block is wrapped in a deferred transaction. + /// + /// let players = try dbPool.read { db in + /// try Player.fetchAll(...) + /// } + /// + /// The block is completely isolated. Eventual concurrent database updates + /// are *not visible* inside the block: + /// + /// try dbPool.read { db in + /// // Those two values are guaranteed to be equal, even if the + /// // `wines` table is modified between the two requests: + /// let count1 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wines")! + /// let count2 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wines")! + /// } + /// + /// try dbPool.read { db in + /// // Now this value may be different: + /// let count = try Int.fetchOne(db, "SELECT COUNT(*) FROM wines")! + /// } + /// + /// This method is *not* reentrant. + /// + /// - parameter block: A block that accesses the database. + /// - throws: The error thrown by the block, or any DatabaseError that would + /// happen while establishing the read access to the database. + public func read<T>(_ block: (Database) throws -> T) throws -> T { + GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.") + return try readerPool.get { reader in + try reader.sync { db in + var result: T? = nil + // The block isolation comes from the DEFERRED transaction. + // See DatabasePoolTests.testReadMethodIsolationOfBlock(). + try db.inTransaction(.deferred) { + result = try block(db) + return .commit + } + return result! + } + } + } + + /// Synchronously executes a read-only block in a protected dispatch queue, + /// and returns its result. + /// + /// The block argument is not isolated: eventual concurrent database updates + /// are visible inside the block: + /// + /// try dbPool.unsafeRead { db in + /// // Those two values may be different because some other thread + /// // may have inserted or deleted a wine between the two requests: + /// let count1 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wines")! + /// let count2 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wines")! + /// } + /// + /// Cursor iteration is safe, though: + /// + /// try dbPool.unsafeRead { db in + /// // No concurrent update can mess with this iteration: + /// let rows = try Row.fetchCursor(db, "SELECT ...") + /// while let row = try rows.next() { ... } + /// } + /// + /// This method is *not* reentrant. + /// + /// - parameter block: A block that accesses the database. + /// - throws: The error thrown by the block, or any DatabaseError that would + /// happen while establishing the read access to the database. + public func unsafeRead<T>(_ block: (Database) throws -> T) throws -> T { + GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.") + return try readerPool.get { reader in + try reader.sync(block) + } + } + + /// Synchronously executes a read-only block in a protected dispatch queue, + /// and returns its result. + /// + /// The block argument is not isolated: eventual concurrent database updates + /// are visible inside the block: + /// + /// try dbPool.unsafeReentrantRead { db in + /// // Those two values may be different because some other thread + /// // may have inserted or deleted a wine between the two requests: + /// let count1 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wines")! + /// let count2 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wines")! + /// } + /// + /// Cursor iteration is safe, though: + /// + /// try dbPool.unsafeReentrantRead { db in + /// // No concurrent update can mess with this iteration: + /// let rows = try Row.fetchCursor(db, "SELECT ...") + /// while let row = try rows.next() { ... } + /// } + /// + /// This method is reentrant. It should be avoided because it fosters + /// dangerous concurrency practices. + /// + /// - parameter block: A block that accesses the database. + /// - throws: The error thrown by the block, or any DatabaseError that would + /// happen while establishing the read access to the database. + public func unsafeReentrantRead<T>(_ block: (Database) throws -> T) throws -> T { + if let reader = currentReader { + return try reader.reentrantSync(block) + } else { + return try readerPool.get { reader in + try reader.sync(block) + } + } + } + + /// Returns a reader that can be used from the current dispatch queue, + /// if any. + private var currentReader: SerializedDatabase? { + var readers: [SerializedDatabase] = [] + readerPool.forEach { reader in + // We can't check for reader.onValidQueue here because + // Pool.forEach() runs its closure argument in some arbitrary + // dispatch queue. We thus extract the reader so that we can query + // it below. + readers.append(reader) + } + + // Now the readers array contains some readers. The pool readers may + // already be different, because some other thread may have started + // a new read, for example. + // + // This doesn't matter: the reader we are looking for is already on + // its own dispatch queue. If it exists, is still in use, thus still + // in the pool, and thus still relevant for our check: + return readers.first { $0.onValidQueue } + } + + + // MARK: - Functions + + /// Add or redefine an SQL function. + /// + /// let fn = DatabaseFunction("succ", argumentCount: 1) { dbValues in + /// guard let int = Int.fromDatabaseValue(dbValues[0]) else { + /// return nil + /// } + /// return int + 1 + /// } + /// dbPool.add(function: fn) + /// try dbPool.read { db in + /// try Int.fetchOne(db, "SELECT succ(1)") // 2 + /// } + public func add(function: DatabaseFunction) { + functions.update(with: function) + write { $0.add(function: function) } + readerPool.forEach { reader in + reader.sync { $0.add(function: function) } + } + } + + /// Remove an SQL function. + public func remove(function: DatabaseFunction) { + functions.remove(function) + write { $0.remove(function: function) } + readerPool.forEach { reader in + reader.sync { $0.remove(function: function) } + } + } + + + // MARK: - Collations + + /// Add or redefine a collation. + /// + /// let collation = DatabaseCollation("localized_standard") { (string1, string2) in + /// return (string1 as NSString).localizedStandardCompare(string2) + /// } + /// dbPool.add(collation: collation) + /// try dbPool.write { db in + /// try db.execute("CREATE TABLE files (name TEXT COLLATE LOCALIZED_STANDARD") + /// } + public func add(collation: DatabaseCollation) { + collations.update(with: collation) + write { $0.add(collation: collation) } + readerPool.forEach { reader in + reader.sync { $0.add(collation: collation) } + } + } + + /// Remove a collation. + public func remove(collation: DatabaseCollation) { + collations.remove(collation) + write { $0.remove(collation: collation) } + readerPool.forEach { reader in + reader.sync { $0.remove(collation: collation) } + } + } +} + + +// ========================================================================= +// MARK: - DatabaseWriter + +extension DatabasePool : DatabaseWriter { + + // MARK: - Writing in Database + + /// Synchronously executes an update block in a protected dispatch queue, + /// and returns its result. + /// + /// Eventual concurrent database updates are postponed until the block + /// has executed. + /// + /// try dbPool.write { db in + /// try db.execute(...) + /// } + /// + /// To maintain database integrity, and preserve eventual concurrent reads + /// from seeing an inconsistent database state, prefer the + /// writeInTransaction method. + /// + /// This method is *not* reentrant. + /// + /// - parameters block: A block that executes SQL statements and return + /// either .commit or .rollback. + /// - throws: The error thrown by the block. + public func write<T>(_ block: (Database) throws -> T) rethrows -> T { + return try writer.sync(block) + } + + /// Synchronously executes a block in a protected dispatch queue, wrapped + /// inside a transaction. + /// + /// Eventual concurrent database updates are postponed until the block + /// has executed. + /// + /// If the block throws an error, the transaction is rollbacked and the + /// error is rethrown. If the block returns .rollback, the transaction is + /// also rollbacked, but no error is thrown. + /// + /// try dbPool.writeInTransaction { db in + /// db.execute(...) + /// return .commit + /// } + /// + /// Eventual concurrent readers do not see partial changes: + /// + /// dbPool.writeInTransaction { db in + /// // Eventually preserve a zero balance + /// try db.execute(db, "INSERT INTO credits ...", arguments: [amount]) + /// try db.execute(db, "INSERT INTO debits ...", arguments: [amount]) + /// } + /// + /// dbPool.read { db in + /// // Here the balance is guaranteed to be zero + /// } + /// + /// This method is *not* reentrant. + /// + /// - parameters: + /// - kind: The transaction type (default nil). If nil, the transaction + /// type is configuration.defaultTransactionKind, which itself + /// defaults to .immediate. See https://www.sqlite.org/lang_transaction.html + /// for more information. + /// - block: A block that executes SQL statements and return either + /// .commit or .rollback. + /// - throws: The error thrown by the block, or any error establishing the + /// transaction. + public func writeInTransaction(_ kind: Database.TransactionKind? = nil, _ block: (Database) throws -> Database.TransactionCompletion) throws { + try write { db in + try db.inTransaction(kind) { + try block(db) + } + } + } + + /// Synchronously executes an update block in a protected dispatch queue, + /// and returns its result. + /// + /// Eventual concurrent database updates are postponed until the block + /// has executed. + /// + /// try dbPool.unsafeReentrantWrite { db in + /// try db.execute(...) + /// } + /// + /// This method is reentrant. It should be avoided because it fosters + /// dangerous concurrency practices. + public func unsafeReentrantWrite<T>(_ block: (Database) throws -> T) rethrows -> T { + return try writer.reentrantSync(block) + } + + + // MARK: - Reading from Database + + /// Asynchronously executes a read-only block in a protected dispatch queue, + /// wrapped in a deferred transaction. + /// + /// This method must be called from the writing dispatch queue. + /// + /// The *block* argument is guaranteed to see the database in the last + /// committed state at the moment this method is called. Eventual concurrent + /// database updates are *not visible* inside the block. + /// + /// try dbPool.write { db in + /// try db.execute("DELETE FROM players") + /// try dbPool.readFromCurrentState { db in + /// // Guaranteed to be zero + /// try Int.fetchOne(db, "SELECT COUNT(*) FROM players")! + /// } + /// try db.execute("INSERT INTO players ...") + /// } + /// + /// This method blocks the current thread until the isolation guarantee has + /// been established, and before the block argument has run. + /// + /// - parameter block: A block that accesses the database. + /// - throws: The error thrown by the block, or any DatabaseError that would + /// happen while establishing the read access to the database. + public func readFromCurrentState(_ block: @escaping (Database) -> Void) throws { + // https://www.sqlite.org/isolation.html + // + // > In WAL mode, SQLite exhibits "snapshot isolation". When a read + // > transaction starts, that reader continues to see an unchanging + // > "snapshot" of the database file as it existed at the moment in time + // > when the read transaction started. Any write transactions that + // > commit while the read transaction is active are still invisible to + // > the read transaction, because the reader is seeing a snapshot of + // > database file from a prior moment in time. + // + // That's exactly what we need. But what does "when read transaction + // starts" mean? + // + // http://www.sqlite.org/lang_transaction.html + // + // > Deferred [transaction] means that no locks are acquired on the + // > database until the database is first accessed. [...] Locks are not + // > acquired until the first read or write operation. [...] Because the + // > acquisition of locks is deferred until they are needed, it is + // > possible that another thread or process could create a separate + // > transaction and write to the database after the BEGIN on the + // > current thread has executed. + // + // Now that's precise enough: SQLite defers "snapshot isolation" until + // the first SELECT: + // + // Reader Writer + // BEGIN DEFERRED TRANSACTION + // UPDATE ... (1) + // Here the change (1) is visible + // SELECT ... + // UPDATE ... (2) + // Here the change (2) is not visible + // + // The readFromCurrentState method says that no change should be visible + // at all. We thus have to perform a select that establishes the + // snapshot isolation before we release the writer queue: + // + // Reader Writer + // BEGIN DEFERRED TRANSACTION + // SELECT anything + // UPDATE ... + // Here the change is not visible by GRDB user + + // This method must be called from the writing dispatch queue: + writer.preconditionValidQueue() + + // The semaphore that blocks the writing dispatch queue until snapshot + // isolation has been established: + let semaphore = DispatchSemaphore(value: 0) + + var readError: Error? = nil + try readerPool.get { reader in + reader.async { db in + do { + try db.beginTransaction(.deferred) + assert(db.isInsideTransaction) + try db.makeSelectStatement("SELECT rootpage FROM sqlite_master").cursor().next() + } catch { + readError = error + semaphore.signal() // Release the writer queue and rethrow error + return + } + semaphore.signal() // We can release the writer queue now that we are isolated for good + block(db) + _ = try? db.commit() // Ignore commit error + } + } + _ = semaphore.wait(timeout: .distantFuture) + if let readError = readError { + // TODO: write a test for this + throw readError + } + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/DatabaseQueue.swift b/Pods/GRDB.swift/GRDB/Core/DatabaseQueue.swift new file mode 100644 index 0000000..a926805 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/DatabaseQueue.swift @@ -0,0 +1,374 @@ +import Foundation + +#if os(iOS) +import UIKit +#endif + +/// A DatabaseQueue serializes access to an SQLite database. +public final class DatabaseQueue { + + // MARK: - Initializers + + /// Opens the SQLite database at path *path*. + /// + /// let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite") + /// + /// Database connections get closed when the database queue gets deallocated. + /// + /// - parameters: + /// - path: The path to the database file. + /// - configuration: A configuration. + /// - throws: A DatabaseError whenever an SQLite error occurs. + public init(path: String, configuration: Configuration = Configuration()) throws { + serializedDatabase = try SerializedDatabase( + path: path, + configuration: configuration, + schemaCache: SimpleDatabaseSchemaCache()) + } + + /// Opens an in-memory SQLite database. + /// + /// let dbQueue = DatabaseQueue() + /// + /// Database memory is released when the database queue gets deallocated. + /// + /// - parameter configuration: A configuration. + public init(configuration: Configuration = Configuration()) { + // Assume SQLite always succeeds creating an in-memory database + serializedDatabase = try! SerializedDatabase( + path: ":memory:", + configuration: configuration, + schemaCache: SimpleDatabaseSchemaCache()) + } + + #if os(iOS) + deinit { + // Undo job done in setupMemoryManagement() + // + // https://developer.apple.com/library/mac/releasenotes/Foundation/RN-Foundation/index.html#10_11Error + // Explicit unregistration is required before iOS 9 and OS X 10.11. + NotificationCenter.default.removeObserver(self) + } + #endif + + + // MARK: - Configuration + + /// The database configuration + public var configuration: Configuration { + return serializedDatabase.configuration + } + + /// The path to the database file; it is ":memory:" for in-memory databases. + public var path: String { + return serializedDatabase.path + } + + + // MARK: - Database access + + /// Synchronously executes a block in a protected dispatch queue, and + /// returns its result. + /// + /// let players = try dbQueue.inDatabase { db in + /// try Player.fetchAll(...) + /// } + /// + /// This method is *not* reentrant. + /// + /// - parameter block: A block that accesses the database. + /// - throws: The error thrown by the block. + public func inDatabase<T>(_ block: (Database) throws -> T) rethrows -> T { + return try serializedDatabase.sync(block) + } + + /// Synchronously executes a block in a protected dispatch queue, wrapped + /// inside a transaction. + /// + /// If the block throws an error, the transaction is rollbacked and the + /// error is rethrown. If the block returns .rollback, the transaction is + /// also rollbacked, but no error is thrown. + /// + /// try dbQueue.inTransaction { db in + /// db.execute(...) + /// return .commit + /// } + /// + /// This method is *not* reentrant. + /// + /// - parameters: + /// - kind: The transaction type (default nil). If nil, the transaction + /// type is configuration.defaultTransactionKind, which itself + /// defaults to .immediate. See https://www.sqlite.org/lang_transaction.html + /// for more information. + /// - block: A block that executes SQL statements and return either + /// .commit or .rollback. + /// - throws: The error thrown by the block. + public func inTransaction(_ kind: Database.TransactionKind? = nil, _ block: (Database) throws -> Database.TransactionCompletion) throws { + try inDatabase { db in + try db.inTransaction(kind) { + try block(db) + } + } + } + + + // MARK: - Memory management + + /// Free as much memory as possible. + /// + /// This method blocks the current thread until all database accesses are completed. + /// + /// See also setupMemoryManagement(application:) + public func releaseMemory() { + inDatabase { $0.releaseMemory() } + } + + + #if os(iOS) + /// Listens to UIApplicationDidEnterBackgroundNotification and + /// UIApplicationDidReceiveMemoryWarningNotification in order to release + /// as much memory as possible. + /// + /// - param application: The UIApplication that will start a background + /// task to let the database queue release its memory when the application + /// enters background. + public func setupMemoryManagement(in application: UIApplication) { + self.application = application + let center = NotificationCenter.default + center.addObserver(self, selector: #selector(DatabaseQueue.applicationDidReceiveMemoryWarning(_:)), name: .UIApplicationDidReceiveMemoryWarning, object: nil) + center.addObserver(self, selector: #selector(DatabaseQueue.applicationDidEnterBackground(_:)), name: .UIApplicationDidEnterBackground, object: nil) + } + + private var application: UIApplication? + + @objc private func applicationDidEnterBackground(_ notification: NSNotification) { + guard let application = application else { + return + } + + var task: UIBackgroundTaskIdentifier! = nil + task = application.beginBackgroundTask(expirationHandler: nil) + + if task == UIBackgroundTaskInvalid { + // Perform releaseMemory() synchronously. + releaseMemory() + } else { + // Perform releaseMemory() asynchronously. + DispatchQueue.global().async { + self.releaseMemory() + application.endBackgroundTask(task) + } + } + } + + @objc private func applicationDidReceiveMemoryWarning(_ notification: NSNotification) { + DispatchQueue.global().async { + self.releaseMemory() + } + } + #endif + + + // MARK: - Not public + + // https://www.sqlite.org/isolation.html + // + // > Within a single database connection X, a SELECT statement always + // > sees all changes to the database that are completed prior to the + // > start of the SELECT statement, whether committed or uncommitted. + // > And the SELECT statement obviously does not see any changes that + // > occur after the SELECT statement completes. But what about changes + // > that occur while the SELECT statement is running? What if a SELECT + // > statement is started and the sqlite3_step() interface steps through + // > roughly half of its output, then some UPDATE statements are run by + // > the application that modify the table that the SELECT statement is + // > reading, then more calls to sqlite3_step() are made to finish out + // > the SELECT statement? Will the later steps of the SELECT statement + // > see the changes made by the UPDATE or not? The answer is that this + // > behavior is undefined. + // + // This is why we use a serialized database: + private var serializedDatabase: SerializedDatabase +} + + +// ========================================================================= +// MARK: - Encryption + +#if SQLITE_HAS_CODEC + extension DatabaseQueue { + + /// Changes the passphrase of an encrypted database + public func change(passphrase: String) throws { + try inDatabase { try $0.change(passphrase: passphrase) } + } + } +#endif + + +// ========================================================================= +// MARK: - DatabaseReader + +extension DatabaseQueue : DatabaseReader { + + // MARK: - DatabaseReader Protocol Adoption + + /// Synchronously executes a read-only block in a protected dispatch queue, + /// and returns its result. + /// + /// let players = try dbQueue.read { db in + /// try Player.fetchAll(...) + /// } + /// + /// This method is *not* reentrant. + /// + /// Starting iOS 8.2, OSX 10.10, and with custom SQLite builds and + /// SQLCipher, attempts to write in the database throw a DatabaseError whose + /// resultCode is `SQLITE_READONLY`. + /// + /// - parameter block: A block that accesses the database. + /// - throws: The error thrown by the block. + public func read<T>(_ block: (Database) throws -> T) rethrows -> T { + // query_only pragma was added in SQLite 3.8.0 http://www.sqlite.org/changes.html#version_3_8_0 + // It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS) + #if GRDBCUSTOMSQLITE || GRDBCIPHER + return try inDatabase { try readOnly($0, block) } + #else + if #available(iOS 8.2, OSX 10.10, *) { + return try inDatabase { try readOnly($0, block) } + } else { + return try inDatabase(block) + } + #endif + } + + /// Alias for `inDatabase`. See `DatabaseReader.unsafeRead`. + public func unsafeRead<T>(_ block: (Database) throws -> T) rethrows -> T { + return try inDatabase(block) + } + + /// Synchronously executes a block in a protected dispatch queue, and + /// returns its result. + /// + /// try dbQueue.unsafeReentrantRead { db in + /// try db.execute(...) + /// } + /// + /// This method is reentrant. It should be avoided because it fosters + /// dangerous concurrency practices. + public func unsafeReentrantRead<T>(_ block: (Database) throws -> T) throws -> T { + return try serializedDatabase.reentrantSync(block) + } + + + // MARK: - Functions + + /// Add or redefine an SQL function. + /// + /// let fn = DatabaseFunction("succ", argumentCount: 1) { dbValues in + /// guard let int = Int.fromDatabaseValue(dbValues[0]) else { + /// return nil + /// } + /// return int + 1 + /// } + /// dbQueue.add(function: fn) + /// try dbQueue.inDatabase { db in + /// try Int.fetchOne(db, "SELECT succ(1)") // 2 + /// } + public func add(function: DatabaseFunction) { + inDatabase { $0.add(function: function) } + } + + /// Remove an SQL function. + public func remove(function: DatabaseFunction) { + inDatabase { $0.remove(function: function) } + } + + + // MARK: - Collations + + /// Add or redefine a collation. + /// + /// let collation = DatabaseCollation("localized_standard") { (string1, string2) in + /// return (string1 as NSString).localizedStandardCompare(string2) + /// } + /// dbQueue.add(collation: collation) + /// try dbQueue.inDatabase { db in + /// try db.execute("CREATE TABLE files (name TEXT COLLATE LOCALIZED_STANDARD") + /// } + public func add(collation: DatabaseCollation) { + inDatabase { $0.add(collation: collation) } + } + + /// Remove a collation. + public func remove(collation: DatabaseCollation) { + inDatabase { $0.remove(collation: collation) } + } +} + + +// ========================================================================= +// MARK: - DatabaseWriter + +extension DatabaseQueue : DatabaseWriter { + + // MARK: - DatabaseWriter Protocol Adoption + + /// Alias for `inDatabase`. See `DatabaseWriter.write`. + public func write<T>(_ block: (Database) throws -> T) rethrows -> T { + return try inDatabase(block) + } + + /// Synchronously executes *block*. + /// + /// Starting iOS 8.2, OSX 10.10, and with custom SQLite builds and + /// SQLCipher, attempts to write in the database throw a DatabaseError whose + /// resultCode is `SQLITE_READONLY`. + /// + /// This method must be called from the protected database dispatch queue. + /// See `DatabaseWriter.readFromCurrentState`. + public func readFromCurrentState(_ block: @escaping (Database) -> Void) { + // query_only pragma was added in SQLite 3.8.0 http://www.sqlite.org/changes.html#version_3_8_0 + // It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS) + #if GRDBCUSTOMSQLITE || GRDBCIPHER + serializedDatabase.execute { readOnly($0, block) } + #else + if #available(iOS 8.2, OSX 10.10, *) { + serializedDatabase.execute { readOnly($0, block) } + } else { + serializedDatabase.execute(block) + } + #endif + } + + /// Synchronously executes a block in a protected dispatch queue, and + /// returns its result. + /// + /// try dbQueue.unsafeReentrantWrite { db in + /// try db.execute(...) + /// } + /// + /// This method is reentrant. It should be avoided because it fosters + /// dangerous concurrency practices. + public func unsafeReentrantWrite<T>(_ block: (Database) throws -> T) rethrows -> T { + return try serializedDatabase.reentrantSync(block) + } +} + +// Wraps the block between two `PRAGMA query_only` statements. +// +// This method is unsafe because the two calls to `PRAGMA query_only` are +// not guaranteed to be serialized (some other thread could mess with this). +private func readOnly<T>(_ db: Database, _ block: (Database) throws -> T) rethrows -> T { + if db.configuration.readonly { + return try block(db) + } else { + try! db.execute("PRAGMA query_only = 1") // Assume can't fail + let result = try block(db) + try! db.execute("PRAGMA query_only = 0") // Assume can't fail + return result + } +} + + diff --git a/Pods/GRDB.swift/GRDB/Core/DatabaseReader.swift b/Pods/GRDB.swift/GRDB/Core/DatabaseReader.swift new file mode 100644 index 0000000..84ee9c4 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/DatabaseReader.swift @@ -0,0 +1,251 @@ +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif + +/// The protocol for all types that can fetch values from a database. +/// +/// It is adopted by DatabaseQueue and DatabasePool. +/// +/// The protocol comes with isolation guarantees that describe the behavior of +/// adopting types in a multithreaded application. +/// +/// Types that adopt the protocol can provide in practice stronger guarantees. +/// For example, DatabaseQueue provides a stronger isolation level +/// than DatabasePool. +/// +/// **Warning**: Isolation guarantees stand as long as there is no external +/// connection to the database. Should you have to cope with external +/// connections, protect yourself with transactions, and be ready to setup a +/// [busy handler](https://www.sqlite.org/c3ref/busy_handler.html). +public protocol DatabaseReader : class { + + // MARK: - Read From Database + + /// Synchronously executes a read-only block that takes a database + /// connection, and returns its result. + /// + /// Guarantee 1: the block argument is isolated. Eventual concurrent + /// database updates are not visible inside the block: + /// + /// try reader.read { db in + /// // Those two values are guaranteed to be equal, even if the + /// // `wines` table is modified between the two requests: + /// let count1 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wines")! + /// let count2 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wines")! + /// } + /// + /// try reader.read { db in + /// // Now this value may be different: + /// let count = try Int.fetchOne(db, "SELECT COUNT(*) FROM wines")! + /// } + /// + /// Guarantee 2: Starting iOS 8.2, OSX 10.10, and with custom SQLite builds + /// and SQLCipher, attempts to write in the database throw a DatabaseError + /// whose resultCode is `SQLITE_READONLY`. + /// + /// - parameter block: A block that accesses the database. + /// - throws: The error thrown by the block, or any DatabaseError that would + /// happen while establishing the read access to the database. + func read<T>(_ block: (Database) throws -> T) throws -> T + + /// Synchronously executes a read-only block that takes a database + /// connection, and returns its result. + /// + /// The two guarantees of the safe `read` method are lifted: + /// + /// The block argument is not isolated: eventual concurrent database updates + /// are visible inside the block: + /// + /// try reader.unsafeRead { db in + /// // Those two values may be different because some other thread + /// // may have inserted or deleted a wine between the two requests: + /// let count1 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wines")! + /// let count2 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wines")! + /// } + /// + /// Cursor iterations are isolated, though: + /// + /// try reader.unsafeRead { db in + /// // No concurrent update can mess with this iteration: + /// let rows = try Row.fetchCursor(db, "SELECT ...") + /// while let row = try rows.next() { ... } + /// } + /// + /// The block argument is not prevented from writing (DatabaseQueue, in + /// particular, will accept database modifications in `unsafeRead`). + /// + /// - parameter block: A block that accesses the database. + /// - throws: The error thrown by the block, or any DatabaseError that would + /// happen while establishing the read access to the database. + func unsafeRead<T>(_ block: (Database) throws -> T) throws -> T + + /// Synchronously executes a block that takes a database connection, and + /// returns its result. + /// + /// The two guarantees of the safe `read` method are lifted: + /// + /// The block argument is not isolated: eventual concurrent database updates + /// are visible inside the block: + /// + /// try reader.unsafeReentrantRead { db in + /// // Those two values may be different because some other thread + /// // may have inserted or deleted a wine between the two requests: + /// let count1 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wines")! + /// let count2 = try Int.fetchOne(db, "SELECT COUNT(*) FROM wines")! + /// } + /// + /// Cursor iterations are isolated, though: + /// + /// try reader.unsafeReentrantRead { db in + /// // No concurrent update can mess with this iteration: + /// let rows = try Row.fetchCursor(db, "SELECT ...") + /// while let row = try rows.next() { ... } + /// } + /// + /// The block argument is not prevented from writing (DatabaseQueue, in + /// particular, will accept database modifications in `unsafeReentrantRead`). + /// + /// - parameter block: A block that accesses the database. + /// - throws: The error thrown by the block, or any DatabaseError that would + /// happen while establishing the read access to the database. + /// + /// This method is reentrant. It should be avoided because it fosters + /// dangerous concurrency practices. + func unsafeReentrantRead<T>(_ block: (Database) throws -> T) throws -> T + + + // MARK: - Functions + + /// Add or redefine an SQL function. + /// + /// let fn = DatabaseFunction("succ", argumentCount: 1) { dbValues in + /// guard let int = Int.fromDatabaseValue(dbValues[0]) else { + /// return nil + /// } + /// return int + 1 + /// } + /// reader.add(function: fn) + /// try reader.read { db in + /// try Int.fetchOne(db, "SELECT succ(1)")! // 2 + /// } + func add(function: DatabaseFunction) + + /// Remove an SQL function. + func remove(function: DatabaseFunction) + + + // MARK: - Collations + + /// Add or redefine a collation. + /// + /// let collation = DatabaseCollation("localized_standard") { (string1, string2) in + /// return (string1 as NSString).localizedStandardCompare(string2) + /// } + /// reader.add(collation: collation) + /// try reader.execute("SELECT * FROM files ORDER BY name COLLATE localized_standard") + func add(collation: DatabaseCollation) + + /// Remove a collation. + func remove(collation: DatabaseCollation) +} + +extension DatabaseReader { + + // MARK: - Backup + + /// Copies the database contents into another database. + /// + /// The `backup` method blocks the current thread until the destination + /// database contains the same contents as the source database. + /// + /// When the source is a DatabasePool, concurrent writes can happen during + /// the backup. Those writes may, or may not, be reflected in the backup, + /// but they won't trigger any error. + public func backup(to writer: DatabaseWriter) throws { + try backup(to: writer, afterBackupInit: nil, afterBackupStep: nil) + } + + func backup(to writer: DatabaseWriter, afterBackupInit: (() -> ())?, afterBackupStep: (() -> ())?) throws { + try read { dbFrom in + try writer.write { dbDest in + guard let backup = sqlite3_backup_init(dbDest.sqliteConnection, "main", dbFrom.sqliteConnection, "main") else { + throw DatabaseError(resultCode: dbDest.lastErrorCode, message: dbDest.lastErrorMessage) + } + guard Int(bitPattern: backup) != Int(SQLITE_ERROR) else { + throw DatabaseError(resultCode: .SQLITE_ERROR) + } + + afterBackupInit?() + + do { + backupLoop: while true { + switch sqlite3_backup_step(backup, -1) { + case SQLITE_DONE: + afterBackupStep?() + break backupLoop + case SQLITE_OK: + afterBackupStep?() + case let code: + throw DatabaseError(resultCode: code, message: dbDest.lastErrorMessage) + } + } + } catch { + sqlite3_backup_finish(backup) + throw error + } + + switch sqlite3_backup_finish(backup) { + case SQLITE_OK: + break + case let code: + throw DatabaseError(resultCode: code, message: dbDest.lastErrorMessage) + } + + dbDest.clearSchemaCache() + } + } + } +} + +/// A type-erased DatabaseReader +/// +/// Instances of AnyDatabaseReader forward their methods to an arbitrary +/// underlying database reader. +public final class AnyDatabaseReader : DatabaseReader { + private let base: DatabaseReader + + /// Creates a database reader that wraps a base database reader. + public init(_ base: DatabaseReader) { + self.base = base + } + + public func read<T>(_ block: (Database) throws -> T) throws -> T { + return try base.read(block) + } + + public func unsafeRead<T>(_ block: (Database) throws -> T) throws -> T { + return try base.unsafeRead(block) + } + + public func unsafeReentrantRead<T>(_ block: (Database) throws -> T) throws -> T { + return try base.unsafeReentrantRead(block) + } + + public func add(function: DatabaseFunction) { + base.add(function: function) + } + + public func remove(function: DatabaseFunction) { + base.remove(function: function) + } + + public func add(collation: DatabaseCollation) { + base.add(collation: collation) + } + + public func remove(collation: DatabaseCollation) { + base.remove(collation: collation) + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/DatabaseSchemaCache.swift b/Pods/GRDB.swift/GRDB/Core/DatabaseSchemaCache.swift new file mode 100644 index 0000000..eded558 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/DatabaseSchemaCache.swift @@ -0,0 +1,109 @@ +/// The protocol for schema cache. +/// +/// This protocol must not contain values that are valid for a single connection +/// only, because several connections can share the same schema cache. +/// +/// Statements can't be cached here, for example. +protocol DatabaseSchemaCache { + mutating func clear() + + func primaryKey(_ table: String) -> PrimaryKeyInfo? + mutating func set(primaryKey: PrimaryKeyInfo, forTable table: String) + + func columns(in table: String) -> [ColumnInfo]? + mutating func set(columns: [ColumnInfo], forTable table: String) + + func indexes(on table: String) -> [IndexInfo]? + mutating func set(indexes: [IndexInfo], forTable table: String) + + func foreignKeys(on table: String) -> [ForeignKeyInfo]? + mutating func set(foreignKeys: [ForeignKeyInfo], forTable table: String) +} + +/// A thread-unsafe database schema cache +final class SimpleDatabaseSchemaCache: DatabaseSchemaCache { + private var primaryKeys: [String: PrimaryKeyInfo] = [:] + private var columns: [String: [ColumnInfo]] = [:] + private var indexes: [String: [IndexInfo]] = [:] + private var foreignKeys: [String: [ForeignKeyInfo]] = [:] + + func clear() { + primaryKeys = [:] + columns = [:] + indexes = [:] + foreignKeys = [:] + } + + func primaryKey(_ table: String) -> PrimaryKeyInfo? { + return primaryKeys[table] + } + + func set(primaryKey: PrimaryKeyInfo, forTable table: String) { + primaryKeys[table] = primaryKey + } + + func columns(in table: String) -> [ColumnInfo]? { + return columns[table] + } + + func set(columns: [ColumnInfo], forTable table: String) { + self.columns[table] = columns + } + + func indexes(on table: String) -> [IndexInfo]? { + return indexes[table] + } + + func set(indexes: [IndexInfo], forTable table: String) { + self.indexes[table] = indexes + } + + func foreignKeys(on table: String) -> [ForeignKeyInfo]? { + return foreignKeys[table] + } + + func set(foreignKeys: [ForeignKeyInfo], forTable table: String) { + self.foreignKeys[table] = foreignKeys + } +} + +/// A thread-safe database schema cache +final class SharedDatabaseSchemaCache: DatabaseSchemaCache { + private let cache = ReadWriteBox(SimpleDatabaseSchemaCache()) + + func clear() { + cache.write { $0.clear() } + } + + func primaryKey(_ table: String) -> PrimaryKeyInfo? { + return cache.read { $0.primaryKey(table) } + } + + func set(primaryKey: PrimaryKeyInfo, forTable table: String) { + cache.write { $0.set(primaryKey: primaryKey, forTable: table) } + } + + func columns(in table: String) -> [ColumnInfo]? { + return cache.read { $0.columns(in: table) } + } + + func set(columns: [ColumnInfo], forTable table: String) { + cache.write { $0.set(columns: columns, forTable: table) } + } + + func indexes(on table: String) -> [IndexInfo]? { + return cache.read { $0.indexes(on: table) } + } + + func set(indexes: [IndexInfo], forTable table: String) { + cache.write { $0.set(indexes: indexes, forTable: table) } + } + + func foreignKeys(on table: String) -> [ForeignKeyInfo]? { + return cache.read { $0.foreignKeys(on: table) } + } + + func set(foreignKeys: [ForeignKeyInfo], forTable table: String) { + cache.write { $0.set(foreignKeys: foreignKeys, forTable: table) } + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/DatabaseValue.swift b/Pods/GRDB.swift/GRDB/Core/DatabaseValue.swift new file mode 100644 index 0000000..a573730 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/DatabaseValue.swift @@ -0,0 +1,377 @@ +import Foundation +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif + +// MARK: - DatabaseValue + +/// DatabaseValue is the intermediate type between SQLite and your values. +/// +/// See https://www.sqlite.org/datatype3.html +public struct DatabaseValue { + + /// An SQLite storage (NULL, INTEGER, REAL, TEXT, BLOB). + public enum Storage : Equatable { + /// The NULL storage class. + case null + + /// The INTEGER storage class, wrapping an Int64. + case int64(Int64) + + /// The REAL storage class, wrapping a Double. + case double(Double) + + /// The TEXT storage class, wrapping a String. + case string(String) + + /// The BLOB storage class, wrapping Data. + case blob(Data) + + /// Returns Int64, Double, String, Data or nil. + public var value: DatabaseValueConvertible? { + switch self { + case .null: + return nil + case .int64(let int64): + return int64 + case .double(let double): + return double + case .string(let string): + return string + case .blob(let data): + return data + } + } + + /// Return true if the storages are identical. + /// + /// Unlike DatabaseValue equality that considers the integer 1 to be + /// equal to the 1.0 double (as SQLite does), int64 and double storages + /// are never equal. + public static func == (_ lhs: Storage, _ rhs: Storage) -> Bool { + switch (lhs, rhs) { + case (.null, .null): return true + case (.int64(let lhs), .int64(let rhs)): return lhs == rhs + case (.double(let lhs), .double(let rhs)): return lhs == rhs + case (.string(let lhs), .string(let rhs)): return lhs == rhs + case (.blob(let lhs), .blob(let rhs)): return lhs == rhs + default: return false + } + } + } + + /// The SQLite storage + public let storage: Storage + + /// The NULL DatabaseValue. + public static let null = DatabaseValue(storage: .null) + + /// Creates a DatabaseValue from Any. + /// + /// The result is nil unless object adopts DatabaseValueConvertible. + public init?(value: Any) { + guard let convertible = value as? DatabaseValueConvertible else { + return nil + } + self = convertible.databaseValue + } + + + // MARK: - Extracting Value + + /// Returns true if databaseValue is NULL. + public var isNull: Bool { + switch storage { + case .null: + return true + default: + return false + } + } + + + // MARK: - Not Public + + init(storage: Storage) { + self.storage = storage + } + + // SQLite function argument + init(sqliteValue: SQLiteValue) { + switch sqlite3_value_type(sqliteValue) { + case SQLITE_NULL: + storage = .null + case SQLITE_INTEGER: + storage = .int64(sqlite3_value_int64(sqliteValue)) + case SQLITE_FLOAT: + storage = .double(sqlite3_value_double(sqliteValue)) + case SQLITE_TEXT: + storage = .string(String(cString: sqlite3_value_text(sqliteValue)!)) + case SQLITE_BLOB: + if let bytes = sqlite3_value_blob(sqliteValue) { + let count = Int(sqlite3_value_bytes(sqliteValue)) + storage = .blob(Data(bytes: bytes, count: count)) // copy bytes + } else { + storage = .blob(Data()) + } + case let type: + // Assume a GRDB bug: there is no point throwing any error. + fatalError("Unexpected SQLite value type: \(type)") + } + } + + /// Returns a DatabaseValue initialized from a raw SQLite statement pointer. + init(sqliteStatement: SQLiteStatement, index: Int32) { + switch sqlite3_column_type(sqliteStatement, Int32(index)) { + case SQLITE_NULL: + storage = .null + case SQLITE_INTEGER: + storage = .int64(sqlite3_column_int64(sqliteStatement, Int32(index))) + case SQLITE_FLOAT: + storage = .double(sqlite3_column_double(sqliteStatement, Int32(index))) + case SQLITE_TEXT: + storage = .string(String(cString: sqlite3_column_text(sqliteStatement, Int32(index)))) + case SQLITE_BLOB: + if let bytes = sqlite3_column_blob(sqliteStatement, Int32(index)) { + let count = Int(sqlite3_column_bytes(sqliteStatement, Int32(index))) + storage = .blob(Data(bytes: bytes, count: count)) // copy bytes + } else { + storage = .blob(Data()) + } + case let type: + // Assume a GRDB bug: there is no point throwing any error. + fatalError("Unexpected SQLite column type: \(type)") + } + } +} + + +// MARK: - Hashable & Equatable + +/// DatabaseValue adopts Hashable. +extension DatabaseValue : Hashable { + + /// The hash value + public var hashValue: Int { + switch storage { + case .null: + return 0 + case .int64(let int64): + // 1 == 1.0, hence 1 and 1.0 must have the same hash: + return Double(int64).hashValue + case .double(let double): + return double.hashValue + case .string(let string): + return string.hashValue + case .blob(let data): + return data.hashValue + } + } + + /// Returns whether two DatabaseValues are equal. + /// + /// 1.databaseValue == "foo".databaseValue // false + /// 1.databaseValue == 1.databaseValue // true + /// + /// When comparing integers and doubles, the result is true if and only + /// values are equal, and if converting one type to the other does + /// not lose information: + /// + /// 1.databaseValue == 1.0.databaseValue // true + /// + /// For a comparison that distinguishes integer and doubles, compare + /// storages instead: + /// + /// 1.databaseValue.storage == 1.0.databaseValue.storage // false + public static func == (lhs: DatabaseValue, rhs: DatabaseValue) -> Bool { + switch (lhs.storage, rhs.storage) { + case (.null, .null): + return true + case (.int64(let lhs), .int64(let rhs)): + return lhs == rhs + case (.double(let lhs), .double(let rhs)): + return lhs == rhs + case (.int64(let lhs), .double(let rhs)): + return Int64(exactly: rhs) == lhs + case (.double(let lhs), .int64(let rhs)): + return rhs == Int64(exactly: lhs) + case (.string(let lhs), .string(let rhs)): + return lhs == rhs + case (.blob(let lhs), .blob(let rhs)): + return lhs == rhs + default: + return false + } + } +} + + +// MARK: - Lossless conversions + +extension DatabaseValue { + /// Converts the database value to the type T. + /// + /// let dbValue = "foo".databaseValue + /// let string = dbValue.losslessConvert() as String // "foo" + /// + /// Conversion is successful if and only if T.fromDatabaseValue returns a + /// non-nil value. + /// + /// This method crashes with a fatal error when conversion fails. + /// + /// let dbValue = "foo".databaseValue + /// let int = dbValue.losslessConvert() as Int // fatalError + /// + /// - parameters: + /// - sql: Optional SQL statement that enhances the eventual + /// conversion error + /// - arguments: Optional statement arguments that enhances the eventual + /// conversion error + public func losslessConvert<T>(sql: String? = nil, arguments: StatementArguments? = nil) -> T where T : DatabaseValueConvertible { + if let value = T.fromDatabaseValue(self) { + return value + } + // Failed conversion: this is data loss, a programmer error. + var error = "could not convert database value \(self) to \(T.self)" + if let sql = sql { + error += " with statement `\(sql)`" + } + if let arguments = arguments, !arguments.isEmpty { + error += " arguments \(arguments)" + } + fatalError(error) + } + + /// Converts the database value to the type Optional<T>. + /// + /// let dbValue = "foo".databaseValue + /// let string = dbValue.losslessConvert() as String? // "foo" + /// let null = DatabaseValue.null.losslessConvert() as String? // nil + /// + /// Conversion is successful if and only if T.fromDatabaseValue returns a + /// non-nil value. + /// + /// This method crashes with a fatal error when conversion fails. + /// + /// let dbValue = "foo".databaseValue + /// let int = dbValue.losslessConvert() as Int? // fatalError + /// + /// - parameters: + /// - sql: Optional SQL statement that enhances the eventual + /// conversion error + /// - arguments: Optional statement arguments that enhances the eventual + /// conversion error + public func losslessConvert<T>(sql: String? = nil, arguments: StatementArguments? = nil) -> T? where T : DatabaseValueConvertible { + // Use fromDatabaseValue first: this allows DatabaseValue to convert NULL to .null. + if let value = T.fromDatabaseValue(self) { + return value + } + if isNull { + // Failed conversion from null: ok + return nil + } else { + // Failed conversion from a non-null database value: this is data + // loss, a programmer error. + var error = "could not convert database value \(self) to \(T.self)" + if let sql = sql { + error += " with statement `\(sql)`" + } + if let arguments = arguments, !arguments.isEmpty { + error += " arguments \(arguments)" + } + fatalError(error) + } + } +} + + +// MARK: - DatabaseValueConvertible & SQLExpressible & SQLExpression + +/// DatabaseValue adopts DatabaseValueConvertible. +extension DatabaseValue : DatabaseValueConvertible { + /// Returns self + public var databaseValue: DatabaseValue { + return self + } + + /// Returns the database value + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> DatabaseValue? { + return dbValue + } +} + +extension DatabaseValue : SQLExpressible { + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public var sqlExpression: SQLExpression { + return self + } +} + +/// DatabaseValue adopts SQLExpression. +extension DatabaseValue : SQLExpression { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func expressionSQL(_ arguments: inout StatementArguments?) -> String { + // fast path for NULL + if isNull { + return "NULL" + } + + if arguments != nil { + arguments!.values.append(self) + return "?" + } else { + // Correctness above all: use SQLite to quote the value. + // Assume that the Quote function always succeeds + return DatabaseQueue().inDatabase { try! String.fetchOne($0, "SELECT QUOTE(?)", arguments: [self])! } + } + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public var negated: SQLExpression { + switch storage { + case .null: + // SELECT NOT NULL -- NULL + return DatabaseValue.null + case .int64(let int64): + return (int64 == 0).sqlExpression + case .double(let double): + return (double == 0.0).sqlExpression + case .string: + // We can't assume all strings are true, and return false: + // + // SELECT NOT '1' -- 0 (because '1' is turned into the integer 1, which is negated into 0) + // SELECT NOT '0' -- 1 (because '0' is turned into the integer 0, which is negated into 1) + return SQLExpressionNot(self) + case .blob: + // We can't assume all blobs are true, and return false: + // + // SELECT NOT X'31' -- 0 (because X'31' is turned into the string '1', then into integer 1, which is negated into 0) + // SELECT NOT X'30' -- 1 (because X'30' is turned into the string '0', then into integer 0, which is negated into 1) + return SQLExpressionNot(self) + } + } +} + +// MARK: - CustomStringConvertible + +/// DatabaseValue adopts CustomStringConvertible. +extension DatabaseValue : CustomStringConvertible { + /// A textual representation of `self`. + public var description: String { + switch storage { + case .null: + return "NULL" + case .int64(let int64): + return String(int64) + case .double(let double): + return String(double) + case .string(let string): + return String(reflecting: string) + case .blob(let data): + return data.description + } + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/DatabaseValueConvertible.swift b/Pods/GRDB.swift/GRDB/Core/DatabaseValueConvertible.swift new file mode 100644 index 0000000..aa102d3 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/DatabaseValueConvertible.swift @@ -0,0 +1,454 @@ +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif + +// MARK: - DatabaseValueConvertible + +/// Types that adopt DatabaseValueConvertible can be initialized from +/// database values. +/// +/// The protocol comes with built-in methods that allow to fetch cursors, +/// arrays, or single values: +/// +/// try String.fetchCursor(db, "SELECT name FROM ...", arguments:...) // Cursor of String +/// try String.fetchAll(db, "SELECT name FROM ...", arguments:...) // [String] +/// try String.fetchOne(db, "SELECT name FROM ...", arguments:...) // String? +/// +/// let statement = try db.makeSelectStatement("SELECT name FROM ...") +/// try String.fetchCursor(statement, arguments:...) // Cursor of String +/// try String.fetchAll(statement, arguments:...) // [String] +/// try String.fetchOne(statement, arguments:...) // String? +/// +/// DatabaseValueConvertible is adopted by Bool, Int, String, etc. +public protocol DatabaseValueConvertible : SQLExpressible { + /// Returns a value that can be stored in the database. + var databaseValue: DatabaseValue { get } + + /// Returns a value initialized from *dbValue*, if possible. + static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? +} + +// SQLExpressible adoption +extension DatabaseValueConvertible { + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public var sqlExpression: SQLExpression { + return databaseValue + } +} + +/// A cursor of database values extracted from a single column. +/// For example: +/// +/// try dbQueue.inDatabase { db in +/// let urls: DatabaseValueCursor<URL> = try URL.fetchCursor(db, "SELECT url FROM links") +/// while let url = urls.next() { // URL +/// print(url) +/// } +/// } +public final class DatabaseValueCursor<Value: DatabaseValueConvertible> : Cursor { + private let statement: SelectStatement + private let sqliteStatement: SQLiteStatement + private let columnIndex: Int32 + private var done = false + + init(statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws { + self.statement = statement + // We'll read from leftmost column at index 0, unless adapter mangles columns + self.columnIndex = try Int32(adapter?.baseColumnIndex(atIndex: 0, layout: statement) ?? 0) + self.sqliteStatement = statement.sqliteStatement + statement.cursorReset(arguments: arguments) + } + + public func next() throws -> Value? { + if done { return nil } + switch sqlite3_step(sqliteStatement) { + case SQLITE_DONE: + done = true + return nil + case SQLITE_ROW: + let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: columnIndex) + return dbValue.losslessConvert() as Value + case let code: + statement.database.selectStatementDidFail(statement) + throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments) + } + } +} + +/// A cursor of optional database values extracted from a single column. +/// For example: +/// +/// try dbQueue.inDatabase { db in +/// let urls: NullableDatabaseValueCursor<URL> = try Optional<URL>.fetchCursor(db, "SELECT url FROM links") +/// while let url = urls.next() { // URL? +/// print(url) +/// } +/// } +public final class NullableDatabaseValueCursor<Value: DatabaseValueConvertible> : Cursor { + private let statement: SelectStatement + private let sqliteStatement: SQLiteStatement + private let columnIndex: Int32 + private var done = false + + init(statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws { + self.statement = statement + // We'll read from leftmost column at index 0, unless adapter mangles columns + self.columnIndex = try Int32(adapter?.baseColumnIndex(atIndex: 0, layout: statement) ?? 0) + self.sqliteStatement = statement.sqliteStatement + statement.cursorReset(arguments: arguments) + } + + public func next() throws -> Value?? { + if done { return nil } + switch sqlite3_step(sqliteStatement) { + case SQLITE_DONE: + done = true + return nil + case SQLITE_ROW: + let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: columnIndex) + return dbValue.losslessConvert() as Value? + case let code: + statement.database.selectStatementDidFail(statement) + throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments) + } + } +} + +/// DatabaseValueConvertible comes with built-in methods that allow to fetch +/// cursors, arrays, or single values: +/// +/// try String.fetchCursor(db, "SELECT name FROM ...", arguments:...) // Cursor of String +/// try String.fetchAll(db, "SELECT name FROM ...", arguments:...) // [String] +/// try String.fetchOne(db, "SELECT name FROM ...", arguments:...) // String? +/// +/// let statement = try db.makeSelectStatement("SELECT name FROM ...") +/// try String.fetchCursor(statement, arguments:...) // Cursor of String +/// try String.fetchAll(statement, arguments:...) // [String] +/// try String.fetchOne(statement, arguments:...) // String +/// +/// DatabaseValueConvertible is adopted by Bool, Int, String, etc. +extension DatabaseValueConvertible { + + + // MARK: Fetching From SelectStatement + + /// Returns a cursor over values fetched from a prepared statement. + /// + /// let statement = try db.makeSelectStatement("SELECT name FROM ...") + /// let names = try String.fetchCursor(statement) // Cursor of String + /// while let name = try names.next() { // String + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - statement: The statement to run. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: A cursor over fetched values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> DatabaseValueCursor<Self> { + return try DatabaseValueCursor(statement: statement, arguments: arguments, adapter: adapter) + } + + /// Returns an array of values fetched from a prepared statement. + /// + /// let statement = try db.makeSelectStatement("SELECT name FROM ...") + /// let names = try String.fetchAll(statement) // [String] + /// + /// - parameters: + /// - statement: The statement to run. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An array. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Self] { + return try Array(fetchCursor(statement, arguments: arguments, adapter: adapter)) + } + + /// Returns a single value fetched from a prepared statement. + /// + /// The result is nil if the query returns no row, or if no value can be + /// extracted from the first row. + /// + /// let statement = try db.makeSelectStatement("SELECT name FROM ...") + /// let name = try String.fetchOne(statement) // String? + /// + /// - parameters: + /// - statement: The statement to run. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An optional value. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchOne(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Self? { + // fetchOne returns nil if there is no row, or if there is a row with a null value + let cursor = try NullableDatabaseValueCursor<Self>(statement: statement, arguments: arguments, adapter: adapter) + return try cursor.next() ?? nil + } +} + +extension DatabaseValueConvertible { + + // MARK: Fetching From Request + + /// Returns a cursor over values fetched from a fetch request. + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn) + /// let names = try String.fetchCursor(db, request) // Cursor of String + /// for let name = try names.next() { // String + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - db: A database connection. + /// - request: A fetch request. + /// - returns: A cursor over fetched values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ db: Database, _ request: Request) throws -> DatabaseValueCursor<Self> { + let (statement, adapter) = try request.prepare(db) + return try fetchCursor(statement, adapter: adapter) + } + + /// Returns an array of values fetched from a fetch request. + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn) + /// let names = try String.fetchAll(db, request) // [String] + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ db: Database, _ request: Request) throws -> [Self] { + let (statement, adapter) = try request.prepare(db) + return try fetchAll(statement, adapter: adapter) + } + + /// Returns a single value fetched from a fetch request. + /// + /// The result is nil if the query returns no row, or if no value can be + /// extracted from the first row. + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn) + /// let name = try String.fetchOne(db, request) // String? + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchOne(_ db: Database, _ request: Request) throws -> Self? { + let (statement, adapter) = try request.prepare(db) + return try fetchOne(statement, adapter: adapter) + } +} + +extension DatabaseValueConvertible { + + // MARK: Fetching From SQL + + /// Returns a cursor over values fetched from an SQL query. + /// + /// let names = try String.fetchCursor(db, "SELECT name FROM ...") // Cursor of String + /// while let name = try name.next() { // String + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: A cursor over fetched values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> DatabaseValueCursor<Self> { + return try fetchCursor(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) + } + + /// Returns an array of values fetched from an SQL query. + /// + /// let names = try String.fetchAll(db, "SELECT name FROM ...") // [String] + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An array. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Self] { + return try fetchAll(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) + } + + /// Returns a single value fetched from an SQL query. + /// + /// The result is nil if the query returns no row, or if no value can be + /// extracted from the first row. + /// + /// let name = try String.fetchOne(db, "SELECT name FROM ...") // String? + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An optional value. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchOne(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Self? { + return try fetchOne(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) + } +} + + +/// Swift's Optional comes with built-in methods that allow to fetch cursors +/// and arrays of optional DatabaseValueConvertible: +/// +/// try Optional<String>.fetchCursor(db, "SELECT name FROM ...", arguments:...) // Cursor of String? +/// try Optional<String>.fetchAll(db, "SELECT name FROM ...", arguments:...) // [String?] +/// +/// let statement = try db.makeSelectStatement("SELECT name FROM ...") +/// try Optional<String>.fetchCursor(statement, arguments:...) // Cursor of String? +/// try Optional<String>.fetchAll(statement, arguments:...) // [String?] +/// +/// DatabaseValueConvertible is adopted by Bool, Int, String, etc. +extension Optional where Wrapped: DatabaseValueConvertible { + + // MARK: Fetching From SelectStatement + + /// Returns a cursor over optional values fetched from a prepared statement. + /// + /// let statement = try db.makeSelectStatement("SELECT name FROM ...") + /// let names = try Optional<String>.fetchCursor(statement) // Cursor of String? + /// while let name = try names.next() { // String? + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - statement: The statement to run. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: A cursor over fetched optional values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> NullableDatabaseValueCursor<Wrapped> { + return try NullableDatabaseValueCursor(statement: statement, arguments: arguments, adapter: adapter) + } + + /// Returns an array of optional values fetched from a prepared statement. + /// + /// let statement = try db.makeSelectStatement("SELECT name FROM ...") + /// let names = try Optional<String>.fetchAll(statement) // [String?] + /// + /// - parameters: + /// - statement: The statement to run. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An array of optional values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Wrapped?] { + return try Array(fetchCursor(statement, arguments: arguments, adapter: adapter)) + } +} + +extension Optional where Wrapped: DatabaseValueConvertible { + + // MARK: Fetching From Request + + /// Returns a cursor over optional values fetched from a fetch request. + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn) + /// let names = try Optional<String>.fetchCursor(db, request) // Cursor of String? + /// while let name = try names.next() { // String? + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - db: A database connection. + /// - requet: A fetch request. + /// - returns: A cursor over fetched optional values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ db: Database, _ request: Request) throws -> NullableDatabaseValueCursor<Wrapped> { + let (statement, adapter) = try request.prepare(db) + return try fetchCursor(statement, adapter: adapter) + } + + /// Returns an array of optional values fetched from a fetch request. + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn) + /// let names = try Optional<String>.fetchAll(db, request) // [String?] + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ db: Database, _ request: Request) throws -> [Wrapped?] { + let (statement, adapter) = try request.prepare(db) + return try fetchAll(statement, adapter: adapter) + } +} + +extension Optional where Wrapped: DatabaseValueConvertible { + + // MARK: Fetching From SQL + + /// Returns a cursor over optional values fetched from an SQL query. + /// + /// let names = try Optional<String>.fetchCursor(db, "SELECT name FROM ...") // Cursor of String? + /// while let name = try names.next() { // String? + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: A cursor over fetched optional values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> NullableDatabaseValueCursor<Wrapped> { + return try fetchCursor(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) + } + + /// Returns an array of optional values fetched from an SQL query. + /// + /// let names = try String.fetchAll(db, "SELECT name FROM ...") // [String?] + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - parameter arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An array of optional values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Wrapped?] { + return try fetchAll(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/DatabaseWriter.swift b/Pods/GRDB.swift/GRDB/Core/DatabaseWriter.swift new file mode 100644 index 0000000..7c8f5b2 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/DatabaseWriter.swift @@ -0,0 +1,178 @@ +/// The protocol for all types that can update a database. +/// +/// It is adopted by DatabaseQueue and DatabasePool. +/// +/// The protocol comes with isolation guarantees that describe the behavior of +/// adopting types in a multithreaded application. +/// +/// Types that adopt the protocol can in practice provide stronger guarantees. +/// For example, DatabaseQueue provides a stronger isolation level +/// than DatabasePool. +/// +/// **Warning**: Isolation guarantees stand as long as there is no external +/// connection to the database. Should you have to cope with external +/// connections, protect yourself with transactions, and be ready to setup a +/// [busy handler](https://www.sqlite.org/c3ref/busy_handler.html). +public protocol DatabaseWriter : DatabaseReader { + + // MARK: - Writing in Database + + /// Synchronously executes a block that takes a database connection, and + /// returns its result. + /// + /// Eventual concurrent database updates are postponed until the block + /// has executed. + /// + /// This method is *not* reentrant. + func write<T>(_ block: (Database) throws -> T) rethrows -> T + + /// Synchronously executes a block that takes a database connection, and + /// returns its result. + /// + /// Eventual concurrent database updates are postponed until the block + /// has executed. + /// + /// This method is reentrant. It should be avoided because it fosters + /// dangerous concurrency practices. + func unsafeReentrantWrite<T>(_ block: (Database) throws -> T) rethrows -> T + + // MARK: - Reading from Database + + /// Synchronously or asynchronously executes a read-only block that takes a + /// database connection. + /// + /// This method must be called from a writing dispatch queue. + /// + /// The *block* argument is guaranteed to see the database in the state it + /// has at the moment this method is called (see below). Eventual concurrent + /// database updates are *not visible* inside the block. + /// + /// What is the "current state" of the database? + /// + /// - When this method is called outside of any transaction, the current + /// state is the last committed state. + /// + /// try writer.write { db in + /// try db.execute("DELETE FROM players") + /// try writer.readFromCurrentState { db in + /// // Guaranteed to be zero + /// try Int.fetchOne(db, "SELECT COUNT(*) FROM players")! + /// } + /// try db.execute("INSERT INTO players ...") + /// } + /// + /// - When this method is called inside an uncommitted transation, the + /// current state depends on the caller: + /// + /// DatabasePool.readFromCurrentState runs *block* asynchronously in a + /// concurrent reader dispatch queue, and release the writing dispatch + /// queue early, before the block has finished. In the example below, + /// the insertion runs concurrently with the select, and the select sees + /// the database in its last committed state. + /// + /// try dbPool.write { db in + /// try db.execute("DELETE FROM players") + /// db.inTransaction { + /// try db.execute("INSERT INTO players ...") + /// try dbPool.readFromCurrentState { db in + /// // Zero + /// try Int.fetchOne(db, "SELECT COUNT(*) FROM players")! + /// } + /// return .commit + /// } + /// } + /// + /// DatabaseQueue.readFromCurrentState simply runs *block* synchronously, + /// and returns when the block has completed. In the example below, the + /// select sees the uncommitted state of the database, and the insertion + /// is run after the select. + /// + /// try dbQueue.write { db in + /// try db.execute("DELETE FROM players") + /// db.inTransaction { + /// try db.execute("INSERT INTO players ...") + /// try dbQueue.readFromCurrentState { db in + /// // One + /// try Int.fetchOne(db, "SELECT COUNT(*) FROM players")! + /// } + /// return .commit + /// } + /// } + /// + /// This method is *not* reentrant. + func readFromCurrentState(_ block: @escaping (Database) -> Void) throws +} + +extension DatabaseWriter { + + // MARK: - Transaction Observers + + /// Add a transaction observer, so that it gets notified of + /// database changes. + /// + /// - parameter transactionObserver: A transaction observer. + /// - parameter extent: The duration of the observation. The default is + /// the observer lifetime (observation lasts until observer + /// is deallocated). + public func add(transactionObserver: TransactionObserver, extent: Database.TransactionObservationExtent = .observerLifetime) { + write { $0.add(transactionObserver: transactionObserver, extent: extent) } + } + + /// Remove a transaction observer. + public func remove(transactionObserver: TransactionObserver) { + write { $0.remove(transactionObserver: transactionObserver) } + } +} + +/// A type-erased DatabaseWriter +/// +/// Instances of AnyDatabaseWriter forward their methods to an arbitrary +/// underlying database writer. +public final class AnyDatabaseWriter : DatabaseWriter { + private let base: DatabaseWriter + + /// Creates a database writer that wraps a base database writer. + public init(_ base: DatabaseWriter) { + self.base = base + } + + public func read<T>(_ block: (Database) throws -> T) throws -> T { + return try base.read(block) + } + + public func unsafeRead<T>(_ block: (Database) throws -> T) throws -> T { + return try base.unsafeRead(block) + } + + public func unsafeReentrantRead<T>(_ block: (Database) throws -> T) throws -> T { + return try base.unsafeReentrantRead(block) + } + + public func add(function: DatabaseFunction) { + base.add(function: function) + } + + public func remove(function: DatabaseFunction) { + base.remove(function: function) + } + + public func add(collation: DatabaseCollation) { + base.add(collation: collation) + } + + public func remove(collation: DatabaseCollation) { + base.remove(collation: collation) + } + + public func write<T>(_ block: (Database) throws -> T) rethrows -> T { + return try base.write(block) + } + + public func unsafeReentrantWrite<T>(_ block: (Database) throws -> T) rethrows -> T { + return try base.unsafeReentrantWrite(block) + } + + public func readFromCurrentState(_ block: @escaping (Database) -> Void) throws { + try base.readFromCurrentState(block) + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/Request.swift b/Pods/GRDB.swift/GRDB/Core/Request.swift new file mode 100644 index 0000000..7f26058 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Request.swift @@ -0,0 +1,556 @@ +/// The protocol for all types that define a way to fetch database rows. +/// +/// Requests can feed the fetching methods of any fetchable type (Row, +/// value, record): +/// +/// let request: Request = ... +/// try Row.fetchCursor(db, request) // RowCursor +/// try String.fetchAll(db, request) // [String] +/// try Player.fetchOne(db, request) // Player? +public protocol Request { + /// A tuple that contains a prepared statement that is ready to be + /// executed, and an eventual row adapter. + func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) + + /// The number of rows fetched by the request. + /// + /// Default implementation builds a naive SQL query based on the statement + /// returned by the `prepare` method: `SELECT COUNT(*) FROM (...)`. + /// + /// Adopting types can refine this countRequest method and return more + /// efficient SQL. + /// + /// - parameter db: A database connection. + func fetchCount(_ db: Database) throws -> Int +} + +extension Request { + /// The number of rows fetched by the request. + /// + /// This default implementation builds a naive SQL query based on the + /// statement returned by the `prepare` method: `SELECT COUNT(*) FROM (...)`. + /// + /// - parameter db: A database connection. + public func fetchCount(_ db: Database) throws -> Int { + let (statement, _) = try prepare(db) + let sql = "SELECT COUNT(*) FROM (\(statement.sql))" + return try Int.fetchOne(db, sql, arguments: statement.arguments)! + } +} + +extension Request { + /// Returns a request bound to type T. + /// + /// The returned request can fetch if the type T is fetchable (Row, + /// value, record). + /// + /// // Int? + /// let maxScore = Player + /// .select(max(scoreColumn)) + /// .asRequest(of: Int.self) // <-- + /// .fetchOne(db) + /// + /// - parameter type: The fetched type T + /// - returns: A typed request bound to type T. + public func asRequest<T>(of type: T.Type) -> AnyTypedRequest<T> { + return AnyTypedRequest { try self.prepare($0) } + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Returns an adapted request. + public func adapted(_ adapter: @escaping (Database) throws -> RowAdapter) -> AdaptedRequest<Self> { + return AdaptedRequest(self, adapter) + } +} + +/// An adapted request. +public struct AdaptedRequest<Base: Request> : Request { + /// Creates an adapted request from a base request and a closure that builds + /// a row adapter from a database connection. + init(_ base: Base, _ adapter: @escaping (Database) throws -> RowAdapter) { + self.base = base + self.adapter = adapter + } + + /// A tuple that contains a prepared statement that is ready to be + /// executed, and an eventual row adapter. + /// + /// - parameter db: A database connection. + public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + let (statement, baseAdapter) = try base.prepare(db) + if let baseAdapter = baseAdapter { + return try (statement, ChainedAdapter(first: baseAdapter, second: adapter(db))) + } else { + return try (statement, adapter(db)) + } + } + + /// The number of rows fetched by the request. + /// + /// - parameter db: A database connection. + public func fetchCount(_ db: Database) throws -> Int { + return try base.fetchCount(db) + } + + private let base: Base + private let adapter: (Database) throws -> RowAdapter +} + +/// A type-erased Request. +/// +/// An instance of AnyRequest forwards its operations to an underlying request, +/// hiding its specifics. +public struct AnyRequest : Request { + /// Creates a new request that wraps and forwards operations to `request`. + public init(_ request: Request) { + self._prepare = { try request.prepare($0) } + } + + /// Creates a new request whose `prepare()` method wraps and forwards + /// operations the argument closure. + public init(_ prepare: @escaping (Database) throws -> (SelectStatement, RowAdapter?)) { + _prepare = prepare + } + + /// A tuple that contains a prepared statement that is ready to be + /// executed, and an eventual row adapter. + /// + /// - parameter db: A database connection. + public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + return try _prepare(db) + } + + private let _prepare: (Database) throws -> (SelectStatement, RowAdapter?) +} + +/// A Request built from raw SQL. +public struct SQLRequest : Request { + /// Creates a new request from an SQL string, optional arguments, and + /// optional row adapter. + /// + /// let request = SQLRequest("SELECT * FROM players") + /// let request = SQLRequest("SELECT * FROM players WHERE id = ?", arguments: [1]) + /// + /// - parameters: + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter. + /// - cached: Defaults to false. If true, the request reuses a cached + /// prepared statement. + /// - returns: A SQLRequest + public init(_ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil, cached: Bool = false) { + self.init(sql, arguments: arguments, adapter: adapter, fromCache: cached ? .user : nil) + } + + /// Creates a new request from an SQL string, optional arguments, and + /// optional row adapter. + /// + /// let request = SQLRequest("SELECT * FROM players") + /// let request = SQLRequest("SELECT * FROM players WHERE id = ?", arguments: [1]) + /// + /// - parameters: + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter. + /// - statementCacheName: Optional statement cache name. + /// - returns: A SQLRequest + init(_ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil, fromCache statementCacheName: Database.StatementCacheName?) { + self.sql = sql + self.arguments = arguments + self.adapter = adapter + self.statementCacheName = statementCacheName + } + + /// A tuple that contains a prepared statement that is ready to be + /// executed, and an eventual row adapter. + /// + /// - parameter db: A database connection. + public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + let statement: SelectStatement + if let statementCacheName = statementCacheName { + statement = try db.selectStatement(sql, fromCache: statementCacheName) + } else { + statement = try db.makeSelectStatement(sql) + } + if let arguments = arguments { + try statement.setArgumentsWithValidation(arguments) + } + return (statement, adapter) + } + + private let sql: String + private let arguments: StatementArguments? + private let adapter: RowAdapter? + private let statementCacheName: Database.StatementCacheName? +} + +/// The protocol for requests that know how to decode database rows. +/// +/// Typed requests can fetch if their associated type RowDecoder is able to +/// decode rows (Row, value, record) +/// +/// struct Player: RowConvertible { ... } +/// let request: ... // Some TypedRequest that fetches Player +/// try request.fetchCursor(db) // Cursor of Player +/// try request.fetchAll(db) // [Player] +/// try request.fetchOne(db) // Player? +public protocol TypedRequest : Request { + + /// The type that can convert raw database rows to fetched values + associatedtype RowDecoder +} + +extension TypedRequest { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Returns an adapted typed request. + public func adapted(_ adapter: @escaping (Database) throws -> RowAdapter) -> AdaptedTypedRequest<Self> { + return AdaptedTypedRequest(self, adapter) + } +} + +/// An adapted typed request. +public struct AdaptedTypedRequest<Base: TypedRequest> : TypedRequest { + + /// The type that can convert raw database rows to fetched values + public typealias RowDecoder = Base.RowDecoder + + /// Creates an adapted request from a base request and a closure that builds + /// a row adapter from a database connection. + init(_ base: Base, _ adapter: @escaping (Database) throws -> RowAdapter) { + adaptedRequest = AdaptedRequest(base, adapter) + } + + /// A tuple that contains a prepared statement that is ready to be + /// executed, and an eventual row adapter. + /// + /// - parameter db: A database connection. + public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + return try adaptedRequest.prepare(db) + } + + /// The number of rows fetched by the request. + /// + /// - parameter db: A database connection. + public func fetchCount(_ db: Database) throws -> Int { + return try adaptedRequest.fetchCount(db) + } + + private let adaptedRequest: AdaptedRequest<Base> +} + +/// A type-erased TypedRequest. +/// +/// An instance of AnyTypedRequest forwards its operations to an underlying +/// typed request, hiding its specifics. +public struct AnyTypedRequest<T> : TypedRequest { + /// The type that can convert raw database rows to fetched values + public typealias RowDecoder = T + + /// Creates a new request that wraps and forwards operations to `request`. + public init<Request>(_ request: Request) where Request: TypedRequest, Request.RowDecoder == RowDecoder { + self._prepare = { try request.prepare($0) } + } + + /// Creates a new request whose `prepare()` method wraps and forwards + /// operations the argument closure. + public init(_ prepare: @escaping (Database) throws -> (SelectStatement, RowAdapter?)) { + _prepare = prepare + } + + /// A tuple that contains a prepared statement that is ready to be + /// executed, and an eventual row adapter. + /// + /// - parameter db: A database connection. + public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + return try _prepare(db) + } + + private let _prepare: (Database) throws -> (SelectStatement, RowAdapter?) +} + +extension TypedRequest where RowDecoder: RowConvertible { + + // MARK: Fetching Record and RowConvertible + + /// A cursor over fetched records. + /// + /// let request: ... // Some TypedRequest that fetches Player + /// let players = try request.fetchCursor(db) // Cursor of Player + /// while let player = try players.next() { // Player + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameter db: A database connection. + /// - returns: A cursor over fetched records. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public func fetchCursor(_ db: Database) throws -> RecordCursor<RowDecoder> { + return try RowDecoder.fetchCursor(db, self) + } + + /// An array of fetched records. + /// + /// let request: ... // Some TypedRequest that fetches Player + /// let players = try request.fetchAll(db) // [Player] + /// + /// - parameter db: A database connection. + /// - returns: An array of records. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public func fetchAll(_ db: Database) throws -> [RowDecoder] { + return try RowDecoder.fetchAll(db, self) + } + + /// The first fetched record. + /// + /// let request: ... // Some TypedRequest that fetches Player + /// let player = try request.fetchOne(db) // Player? + /// + /// - parameter db: A database connection. + /// - returns: An optional record. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public func fetchOne(_ db: Database) throws -> RowDecoder? { + return try RowDecoder.fetchOne(db, self) + } +} + +extension TypedRequest where RowDecoder: DatabaseValueConvertible { + + // MARK: Fetching Values + + /// A cursor over fetched values. + /// + /// let request: ... // Some TypedRequest that fetches String + /// let strings = try request.fetchCursor(db) // Cursor of String + /// while let string = try strings.next() { // String + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameter db: A database connection. + /// - returns: A cursor over fetched values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public func fetchCursor(_ db: Database) throws -> DatabaseValueCursor<RowDecoder> { + return try RowDecoder.fetchCursor(db, self) + } + + /// An array of fetched values. + /// + /// let request: ... // Some TypedRequest that fetches String + /// let strings = try request.fetchAll(db) // [String] + /// + /// - parameter db: A database connection. + /// - returns: An array of values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public func fetchAll(_ db: Database) throws -> [RowDecoder] { + return try RowDecoder.fetchAll(db, self) + } + + /// The first fetched value. + /// + /// The result is nil if the request returns no row, or if no value can be + /// extracted from the first row. + /// + /// let request: ... // Some TypedRequest that fetches String + /// let string = try request.fetchOne(db) // String? + /// + /// - parameter db: A database connection. + /// - returns: An optional value. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public func fetchOne(_ db: Database) throws -> RowDecoder? { + return try RowDecoder.fetchOne(db, self) + } +} + +extension TypedRequest where RowDecoder: DatabaseValueConvertible & StatementColumnConvertible { + + // MARK: Fetching Values + + /// A cursor over fetched values. + /// + /// let request: ... // Some TypedRequest that fetches String + /// let strings = try request.fetchCursor(db) // Cursor of String + /// while let string = try strings.next() { // String + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameter db: A database connection. + /// - returns: A cursor over fetched values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public func fetchCursor(_ db: Database) throws -> ColumnCursor<RowDecoder> { + return try RowDecoder.fetchCursor(db, self) + } + + /// An array of fetched values. + /// + /// let request: ... // Some TypedRequest that fetches String + /// let strings = try request.fetchAll(db) // [String] + /// + /// - parameter db: A database connection. + /// - returns: An array of values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public func fetchAll(_ db: Database) throws -> [RowDecoder] { + return try RowDecoder.fetchAll(db, self) + } + + /// The first fetched value. + /// + /// The result is nil if the request returns no row, or if no value can be + /// extracted from the first row. + /// + /// let request: ... // Some TypedRequest that fetches String + /// let string = try request.fetchOne(db) // String? + /// + /// - parameter db: A database connection. + /// - returns: An optional value. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public func fetchOne(_ db: Database) throws -> RowDecoder? { + return try RowDecoder.fetchOne(db, self) + } +} + +extension TypedRequest where RowDecoder: _OptionalProtocol, RowDecoder._Wrapped: DatabaseValueConvertible { + + // MARK: Fetching Optional values + + /// A cursor over fetched optional values. + /// + /// let request: ... // Some TypedRequest that fetches Optional<String> + /// let strings = try request.fetchCursor(db) // Cursor of String? + /// while let string = try strings.next() { // String? + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameter db: A database connection. + /// - returns: A cursor over fetched values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public func fetchCursor(_ db: Database) throws -> NullableDatabaseValueCursor<RowDecoder._Wrapped> { + return try Optional<RowDecoder._Wrapped>.fetchCursor(db, self) + } + + /// An array of fetched optional values. + /// + /// let request: ... // Some TypedRequest that fetches Optional<String> + /// let strings = try request.fetchAll(db) // [String?] + /// + /// - parameter db: A database connection. + /// - returns: An array of values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public func fetchAll(_ db: Database) throws -> [RowDecoder._Wrapped?] { + return try Optional<RowDecoder._Wrapped>.fetchAll(db, self) + } +} + +extension TypedRequest where RowDecoder: _OptionalProtocol, RowDecoder._Wrapped: DatabaseValueConvertible & StatementColumnConvertible { + + // MARK: Fetching Optional values + + /// A cursor over fetched optional values. + /// + /// let request: ... // Some TypedRequest that fetches Optional<String> + /// let strings = try request.fetchCursor(db) // Cursor of String? + /// while let string = try strings.next() { // String? + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameter db: A database connection. + /// - returns: A cursor over fetched values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public func fetchCursor(_ db: Database) throws -> NullableColumnCursor<RowDecoder._Wrapped> { + return try Optional<RowDecoder._Wrapped>.fetchCursor(db, self) + } + + /// An array of fetched optional values. + /// + /// let request: ... // Some TypedRequest that fetches Optional<String> + /// let strings = try request.fetchAll(db) // [String?] + /// + /// - parameter db: A database connection. + /// - returns: An array of values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public func fetchAll(_ db: Database) throws -> [RowDecoder._Wrapped?] { + return try Optional<RowDecoder._Wrapped>.fetchAll(db, self) + } +} + +extension TypedRequest where RowDecoder: Row { + + // MARK: Fetching Rows + + /// A cursor over fetched rows. + /// + /// let request: ... // Some TypedRequest that fetches Row + /// let rows = try request.fetchCursor(db) // RowCursor + /// while let row = try rows.next() { // Row + /// let id: Int64 = row[0] + /// let name: String = row[1] + /// } + /// + /// Fetched rows are reused during the cursor iteration: don't turn a row + /// cursor into an array with `Array(rows)` or `rows.filter { ... }` since + /// you would not get the distinct rows you expect. Use `Row.fetchAll(...)` + /// instead. + /// + /// For the same reason, make sure you make a copy whenever you extract a + /// row for later use: `row.copy()`. + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameter db: A database connection. + /// - returns: A cursor over fetched rows. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public func fetchCursor(_ db: Database) throws -> RowCursor { + return try Row.fetchCursor(db, self) + } + + /// An array of fetched rows. + /// + /// let request: ... // Some TypedRequest that fetches Row + /// let rows = try request.fetchAll(db) + /// + /// - parameter db: A database connection. + /// - returns: An array of fetched rows. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public func fetchAll(_ db: Database) throws -> [Row] { + return try Row.fetchAll(db, self) + } + + /// The first fetched row. + /// + /// let request: ... // Some TypedRequest that fetches Row + /// let row = try request.fetchOne(db) + /// + /// - parameter db: A database connection. + /// - returns: A,n optional rows. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public func fetchOne(_ db: Database) throws -> Row? { + return try Row.fetchOne(db, self) + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/Row.swift b/Pods/GRDB.swift/GRDB/Core/Row.swift new file mode 100644 index 0000000..540130e --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Row.swift @@ -0,0 +1,1094 @@ +import Foundation +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif + +/// A database row. +public final class Row { + /// The number of columns in the row. + public let count: Int + + // MARK: - Building rows + + /// Creates an empty row. + public convenience init() { + self.init(impl: EmptyRowImpl()) + } + + /// Creates a row from a dictionary of values. + public convenience init(_ dictionary: [String: DatabaseValueConvertible?]) { + self.init(impl: ArrayRowImpl(columns: dictionary.map { ($0, $1?.databaseValue ?? .null) })) + } + + /// Creates a row from [AnyHashable: Any]. + /// + /// The result is nil unless all dictionary keys are strings, and values + /// adopt DatabaseValueConvertible. + public convenience init?(_ dictionary: [AnyHashable: Any]) { + var initDictionary = [String: DatabaseValueConvertible?]() + for (key, value) in dictionary { + guard let columnName = key as? String else { + return nil + } + guard let dbValue = DatabaseValue(value: value) else { + return nil + } + initDictionary[columnName] = dbValue + } + self.init(initDictionary) + } + + /// Returns an immutable copy of the row. + /// + /// For performance reasons, rows fetched from a cursor are reused during + /// the iteration of a query: make sure to make a copy of it whenever you + /// want to keep a specific one: `row.copy()`. + public func copy() -> Row { + return impl.copy(self) + } + + // MARK: - Not Public + + let impl: RowImpl + + /// Returns true if and only if the row was fetched from a database. + var isFetched: Bool { + return impl.isFetched + } + + /// Unless we are producing a row array, we use a single row when iterating + /// a statement: + /// + /// let rows = try Row.fetchCursor(db, "SELECT ...") + /// let players = try Player.fetchAll(db, "SELECT ...") + /// + /// This row keeps an unmanaged reference to the statement, and a handle to + /// the sqlite statement, so that we avoid many retain/release invocations. + /// + /// The statementRef is released in deinit. + let statementRef: Unmanaged<SelectStatement>? + let sqliteStatement: SQLiteStatement? + + deinit { + statementRef?.release() + } + + /// Creates a row that maps an SQLite statement. Further calls to + /// sqlite3_step() modify the row. + /// + /// The row is implemented on top of StatementRowImpl, which grants *direct* + /// access to the SQLite statement. Iteration of the statement does modify + /// the row. + init(statement: SelectStatement) { + let statementRef = Unmanaged.passRetained(statement) // released in deinit + self.statementRef = statementRef + self.sqliteStatement = statement.sqliteStatement + self.impl = StatementRowImpl(sqliteStatement: statement.sqliteStatement, statementRef: statementRef) + self.count = Int(sqlite3_column_count(sqliteStatement)) + } + + /// Creates a row that contain a copy of the current state of the + /// SQLite statement. Further calls to sqlite3_step() do not modify the row. + /// + /// The row is implemented on top of StatementCopyRowImpl, which *copies* + /// the values from the SQLite statement so that further iteration of the + /// statement does not modify the row. + convenience init(copiedFromSQLiteStatement sqliteStatement: SQLiteStatement, statementRef: Unmanaged<SelectStatement>) { + self.init(impl: StatementCopyRowImpl(sqliteStatement: sqliteStatement, columnNames: statementRef.takeUnretainedValue().columnNames)) + } + + init(impl: RowImpl) { + self.impl = impl + self.count = impl.count + self.statementRef = nil + self.sqliteStatement = nil + } +} + +extension Row { + + // MARK: - Columns + + /// The names of columns in the row. + /// + /// Columns appear in the same order as they occur as the `.0` member + /// of column-value pairs in `self`. + public var columnNames: LazyMapCollection<Row, String> { + return lazy.map { $0.0 } + } + + /// Returns true if and only if the row has that column. + /// + /// This method is case-insensitive. + public func hasColumn(_ columnName: String) -> Bool { + return impl.index(ofColumn: columnName) != nil + } +} + +extension Row { + + // MARK: - Extracting Values + + /// Returns Int64, Double, String, Data or nil, depending on the value + /// stored at the given index. + /// + /// Indexes span from 0 for the leftmost column to (row.count - 1) for the + /// righmost column. + public subscript(_ index: Int) -> DatabaseValueConvertible? { + GRDBPrecondition(index >= 0 && index < count, "row index out of range") + return impl.databaseValue(atUncheckedIndex: index).storage.value + } + + /// Returns the value at given index, converted to the requested type. + /// + /// Indexes span from 0 for the leftmost column to (row.count - 1) for the + /// righmost column. + /// + /// If the SQLite value is NULL, the result is nil. Otherwise the SQLite + /// value is converted to the requested type `Value`. Should this conversion + /// fail, a fatal error is raised. + public subscript<Value: DatabaseValueConvertible>(_ index: Int) -> Value? { + GRDBPrecondition(index >= 0 && index < count, "row index out of range") + return impl.databaseValue(atUncheckedIndex: index).losslessConvert() + } + + /// Returns the value at given index, converted to the requested type. + /// + /// Indexes span from 0 for the leftmost column to (row.count - 1) for the + /// righmost column. + /// + /// If the SQLite value is NULL, the result is nil. Otherwise the SQLite + /// value is converted to the requested type `Value`. Should this conversion + /// fail, a fatal error is raised. + /// + /// This method exists as an optimization opportunity for types that adopt + /// StatementColumnConvertible. It *may* trigger SQLite built-in conversions + /// (see https://www.sqlite.org/datatype3.html). + public subscript<Value: DatabaseValueConvertible & StatementColumnConvertible>(_ index: Int) -> Value? { + GRDBPrecondition(index >= 0 && index < count, "row index out of range") + if let sqliteStatement = sqliteStatement { // fast path + return Row.statementColumnConvertible(atUncheckedIndex: Int32(index), in: sqliteStatement) + } + return impl.databaseValue(atUncheckedIndex: index).losslessConvert() + } + + /// Returns the value at given index, converted to the requested type. + /// + /// Indexes span from 0 for the leftmost column to (row.count - 1) for the + /// righmost column. + /// + /// This method crashes if the fetched SQLite value is NULL, or if the + /// SQLite value can not be converted to `Value`. + public subscript<Value: DatabaseValueConvertible>(_ index: Int) -> Value { + GRDBPrecondition(index >= 0 && index < count, "row index out of range") + return impl.databaseValue(atUncheckedIndex: index).losslessConvert() + } + + /// Returns the value at given index, converted to the requested type. + /// + /// Indexes span from 0 for the leftmost column to (row.count - 1) for the + /// righmost column. + /// + /// This method crashes if the fetched SQLite value is NULL, or if the + /// SQLite value can not be converted to `Value`. + /// + /// This method exists as an optimization opportunity for types that adopt + /// StatementColumnConvertible. It *may* trigger SQLite built-in conversions + /// (see https://www.sqlite.org/datatype3.html). + public subscript<Value: DatabaseValueConvertible & StatementColumnConvertible>(_ index: Int) -> Value { + GRDBPrecondition(index >= 0 && index < count, "row index out of range") + if let sqliteStatement = sqliteStatement { // fast path + return Row.statementColumnConvertible(atUncheckedIndex: Int32(index), in: sqliteStatement) + } + return impl.databaseValue(atUncheckedIndex: index).losslessConvert() + } + + /// Returns Int64, Double, String, Data or nil, depending on the value + /// stored at the given column. + /// + /// Column name lookup is case-insensitive, and when several columns have + /// the same name, the leftmost column is considered. + /// + /// The result is nil if the row does not contain the column. + public subscript(_ columnName: String) -> DatabaseValueConvertible? { + // IMPLEMENTATION NOTE + // This method has a single know use case: checking if the value is nil, + // as in: + // + // if row["foo"] != nil { ... } + // + // Without this method, the code above would not compile. + guard let index = impl.index(ofColumn: columnName) else { + return nil + } + return impl.databaseValue(atUncheckedIndex: index).storage.value + } + + /// Returns the value at given column, converted to the requested type. + /// + /// Column name lookup is case-insensitive, and when several columns have + /// the same name, the leftmost column is considered. + /// + /// If the column is missing or if the SQLite value is NULL, the result is + /// nil. Otherwise the SQLite value is converted to the requested type + /// `Value`. Should this conversion fail, a fatal error is raised. + public subscript<Value: DatabaseValueConvertible>(_ columnName: String) -> Value? { + guard let index = impl.index(ofColumn: columnName) else { + return nil + } + return impl.databaseValue(atUncheckedIndex: index).losslessConvert() + } + + /// Returns the value at given column, converted to the requested type. + /// + /// Column name lookup is case-insensitive, and when several columns have + /// the same name, the leftmost column is considered. + /// + /// If the column is missing or if the SQLite value is NULL, the result is + /// nil. Otherwise the SQLite value is converted to the requested type + /// `Value`. Should this conversion fail, a fatal error is raised. + /// + /// This method exists as an optimization opportunity for types that adopt + /// StatementColumnConvertible. It *may* trigger SQLite built-in conversions + /// (see https://www.sqlite.org/datatype3.html). + public subscript<Value: DatabaseValueConvertible & StatementColumnConvertible>(_ columnName: String) -> Value? { + guard let index = impl.index(ofColumn: columnName) else { + return nil + } + if let sqliteStatement = sqliteStatement { // fast path + return Row.statementColumnConvertible(atUncheckedIndex: Int32(index), in: sqliteStatement) + } + return impl.databaseValue(atUncheckedIndex: index).losslessConvert() + } + + /// Returns the value at given column, converted to the requested type. + /// + /// Column name lookup is case-insensitive, and when several columns have + /// the same name, the leftmost column is considered. + /// + /// If the row does not contain the column, a fatal error is raised. + /// + /// This method crashes if the fetched SQLite value is NULL, or if the + /// SQLite value can not be converted to `Value`. + public subscript<Value: DatabaseValueConvertible>(_ columnName: String) -> Value { + guard let index = impl.index(ofColumn: columnName) else { + // Programmer error + fatalError("no such column: \(columnName)") + } + return impl.databaseValue(atUncheckedIndex: index).losslessConvert() + } + + /// Returns the value at given column, converted to the requested type. + /// + /// Column name lookup is case-insensitive, and when several columns have + /// the same name, the leftmost column is considered. + /// + /// If the row does not contain the column, a fatal error is raised. + /// + /// This method crashes if the fetched SQLite value is NULL, or if the + /// SQLite value can not be converted to `Value`. + /// + /// This method exists as an optimization opportunity for types that adopt + /// StatementColumnConvertible. It *may* trigger SQLite built-in conversions + /// (see https://www.sqlite.org/datatype3.html). + public subscript<Value: DatabaseValueConvertible & StatementColumnConvertible>(_ columnName: String) -> Value { + guard let index = impl.index(ofColumn: columnName) else { + // Programmer error + fatalError("no such column: \(columnName)") + } + if let sqliteStatement = sqliteStatement { // fast path + return Row.statementColumnConvertible(atUncheckedIndex: Int32(index), in: sqliteStatement) + } + return impl.databaseValue(atUncheckedIndex: index).losslessConvert() + } + + /// Returns Int64, Double, String, NSData or nil, depending on the value + /// stored at the given column. + /// + /// Column name lookup is case-insensitive, and when several columns have + /// the same name, the leftmost column is considered. + /// + /// The result is nil if the row does not contain the column. + public subscript(_ column: Column) -> DatabaseValueConvertible? { + return self[column.name] + } + + /// Returns the value at given column, converted to the requested type. + /// + /// Column name lookup is case-insensitive, and when several columns have + /// the same name, the leftmost column is considered. + /// + /// If the column is missing or if the SQLite value is NULL, the result is + /// nil. Otherwise the SQLite value is converted to the requested type + /// `Value`. Should this conversion fail, a fatal error is raised. + public subscript<Value: DatabaseValueConvertible>(_ column: Column) -> Value? { + return self[column.name] + } + + /// Returns the value at given column, converted to the requested type. + /// + /// Column name lookup is case-insensitive, and when several columns have + /// the same name, the leftmost column is considered. + /// + /// If the column is missing or if the SQLite value is NULL, the result is + /// nil. Otherwise the SQLite value is converted to the requested type + /// `Value`. Should this conversion fail, a fatal error is raised. + /// + /// This method exists as an optimization opportunity for types that adopt + /// StatementColumnConvertible. It *may* trigger SQLite built-in conversions + /// (see https://www.sqlite.org/datatype3.html). + public subscript<Value: DatabaseValueConvertible & StatementColumnConvertible>(_ column: Column) -> Value? { + return self[column.name] + } + + /// Returns the value at given column, converted to the requested type. + /// + /// Column name lookup is case-insensitive, and when several columns have + /// the same name, the leftmost column is considered. + /// + /// If the row does not contain the column, a fatal error is raised. + /// + /// This method crashes if the fetched SQLite value is NULL, or if the + /// SQLite value can not be converted to `Value`. + public subscript<Value: DatabaseValueConvertible>(_ column: Column) -> Value { + return self[column.name] + } + + /// Returns the value at given column, converted to the requested type. + /// + /// Column name lookup is case-insensitive, and when several columns have + /// the same name, the leftmost column is considered. + /// + /// If the row does not contain the column, a fatal error is raised. + /// + /// This method crashes if the fetched SQLite value is NULL, or if the + /// SQLite value can not be converted to `Value`. + /// + /// This method exists as an optimization opportunity for types that adopt + /// StatementColumnConvertible. It *may* trigger SQLite built-in conversions + /// (see https://www.sqlite.org/datatype3.html). + public subscript<Value: DatabaseValueConvertible & StatementColumnConvertible>(_ column: Column) -> Value { + return self[column.name] + } + + /// Returns the optional Data at given index. + /// + /// Indexes span from 0 for the leftmost column to (row.count - 1) for the + /// righmost column. + /// + /// If the SQLite value is NULL, the result is nil. If the SQLite value can + /// not be converted to Data, a fatal error is raised. + /// + /// The returned data does not owns its bytes: it must not be used longer + /// than the row's lifetime. + public func dataNoCopy(atIndex index: Int) -> Data? { + GRDBPrecondition(index >= 0 && index < count, "row index out of range") + return impl.dataNoCopy(atUncheckedIndex: index) + } + + /// Returns the optional Data at given column. + /// + /// Column name lookup is case-insensitive, and when several columns have + /// the same name, the leftmost column is considered. + /// + /// If the column is missing or if the SQLite value is NULL, the result is + /// nil. If the SQLite value can not be converted to Data, a fatal error + /// is raised. + /// + /// The returned data does not owns its bytes: it must not be used longer + /// than the row's lifetime. + public func dataNoCopy(named columnName: String) -> Data? { + guard let index = impl.index(ofColumn: columnName) else { + return nil + } + return impl.dataNoCopy(atUncheckedIndex: index) + } + + /// Returns the optional `NSData` at given column. + /// + /// Column name lookup is case-insensitive, and when several columns have + /// the same name, the leftmost column is considered. + /// + /// If the column is missing or if the SQLite value is NULL, the result is + /// nil. If the SQLite value can not be converted to NSData, a fatal error + /// is raised. + /// + /// The returned data does not owns its bytes: it must not be used longer + /// than the row's lifetime. + public func dataNoCopy(_ column: Column) -> Data? { + return dataNoCopy(named: column.name) + } +} + +extension Row { + + // MARK: - Extracting DatabaseValue + + /// The database values in the row. + /// + /// Values appear in the same order as they occur as the `.1` member + /// of column-value pairs in `self`. + public var databaseValues: LazyMapCollection<Row, DatabaseValue> { + return lazy.map { $0.1 } + } +} + +extension Row { + + // MARK: - Helpers + @inline(__always) + private static func statementColumnConvertible<Value: StatementColumnConvertible>(atUncheckedIndex index: Int32, in sqliteStatement: SQLiteStatement) -> Value? { + guard sqlite3_column_type(sqliteStatement, index) != SQLITE_NULL else { + return nil + } + return Value.init(sqliteStatement: sqliteStatement, index: index) + } + + @inline(__always) + private static func statementColumnConvertible<Value: StatementColumnConvertible>(atUncheckedIndex index: Int32, in sqliteStatement: SQLiteStatement) -> Value { + guard sqlite3_column_type(sqliteStatement, index) != SQLITE_NULL else { + // Programmer error + fatalError("could not convert database value NULL to \(Value.self)") + } + return Value.init(sqliteStatement: sqliteStatement, index: index) + } +} + +extension Row { + + // MARK: - Scopes + + /// Returns a scoped row, if the row was fetched along with a row adapter + /// that defines this scope. + /// + /// // Two adapters + /// let fooAdapter = ColumnMapping(["value": "foo"]) + /// let barAdapter = ColumnMapping(["value": "bar"]) + /// + /// // Define scopes + /// let adapter = ScopeAdapter([ + /// "foo": fooAdapter, + /// "bar": barAdapter]) + /// + /// // Fetch + /// let sql = "SELECT 'foo' AS foo, 'bar' AS bar" + /// let row = try Row.fetchOne(db, sql, adapter: adapter)! + /// + /// // Scoped rows: + /// if let fooRow = row.scoped(on: "foo") { + /// fooRow["value"] // "foo" + /// } + /// if let barRow = row.scopeed(on: "bar") { + /// barRow["value"] // "bar" + /// } + public func scoped(on name: String) -> Row? { + return impl.scoped(on: name) + } +} + +/// A cursor of database rows. For example: +/// +/// try dbQueue.inDatabase { db in +/// let rows: RowCursor = try Row.fetchCursor(db, "SELECT * FROM players") +/// } +public final class RowCursor : Cursor { + public let statement: SelectStatement + private let sqliteStatement: SQLiteStatement + private let row: Row // Reused for performance + private var done = false + + init(statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws { + self.statement = statement + self.row = try Row(statement: statement).adapted(with: adapter, layout: statement) + self.sqliteStatement = statement.sqliteStatement + statement.cursorReset(arguments: arguments) + } + + public func next() throws -> Row? { + if done { return nil } + switch sqlite3_step(sqliteStatement) { + case SQLITE_DONE: + done = true + return nil + case SQLITE_ROW: + return row + case let code: + statement.database.selectStatementDidFail(statement) + throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments) + } + } +} + +extension Row { + + // MARK: - Fetching From SelectStatement + + /// Returns a cursor over rows fetched from a prepared statement. + /// + /// let statement = try db.makeSelectStatement("SELECT ...") + /// let rows = try Row.fetchCursor(statement) // RowCursor + /// while let row = try rows.next() { // Row + /// let id: Int64 = row[0] + /// let name: String = row[1] + /// } + /// + /// Fetched rows are reused during the cursor iteration: don't turn a row + /// cursor into an array with `Array(rows)` or `rows.filter { ... }` since + /// you would not get the distinct rows you expect. Use `Row.fetchAll(...)` + /// instead. + /// + /// For the same reason, make sure you make a copy whenever you extract a + /// row for later use: `row.copy()`. + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: A cursor over fetched rows. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> RowCursor { + return try RowCursor(statement: statement, arguments: arguments, adapter: adapter) + } + + /// Returns an array of rows fetched from a prepared statement. + /// + /// let statement = try db.makeSelectStatement("SELECT ...") + /// let rows = try Row.fetchAll(statement) + /// + /// - parameters: + /// - statement: The statement to run. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An array of rows. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Row] { + // The cursor reuses a single mutable row. Return immutable copies. + return try Array(fetchCursor(statement, arguments: arguments, adapter: adapter).map { $0.copy() }) + } + + /// Returns a single row fetched from a prepared statement. + /// + /// let statement = try db.makeSelectStatement("SELECT ...") + /// let row = try Row.fetchOne(statement) + /// + /// - parameters: + /// - statement: The statement to run. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An optional row. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchOne(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Row? { + // The cursor reuses a single mutable row. Return an immutable copy. + return try fetchCursor(statement, arguments: arguments, adapter: adapter).next().map { $0.copy() } + } +} + +extension Row { + + // MARK: - Fetching From Request + + /// Returns a cursor over rows fetched from a fetch request. + /// + /// let idColumn = Column("id") + /// let nameColumn = Column("name") + /// let request = Player.select(idColumn, nameColumn) + /// let rows = try Row.fetchCursor(db) // RowCursor + /// while let row = try rows.next() { // Row + /// let id: Int64 = row[0] + /// let name: String = row[1] + /// } + /// + /// Fetched rows are reused during the cursor iteration: don't turn a row + /// cursor into an array with `Array(rows)` or `rows.filter { ... }` since + /// you would not get the distinct rows you expect. Use `Row.fetchAll(...)` + /// instead. + /// + /// For the same reason, make sure you make a copy whenever you extract a + /// row for later use: `row.copy()`. + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - db: A database connection. + /// - request: A fetch request. + /// - returns: A cursor over fetched rows. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ db: Database, _ request: Request) throws -> RowCursor { + let (statement, adapter) = try request.prepare(db) + return try fetchCursor(statement, adapter: adapter) + } + + /// Returns an array of rows fetched from a fetch request. + /// + /// let idColumn = Column("id") + /// let nameColumn = Column("name") + /// let request = Player.select(idColumn, nameColumn) + /// let rows = try Row.fetchAll(db, request) + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ db: Database, _ request: Request) throws -> [Row] { + let (statement, adapter) = try request.prepare(db) + return try fetchAll(statement, adapter: adapter) + } + + /// Returns a single row fetched from a fetch request. + /// + /// let idColumn = Column("id") + /// let nameColumn = Column("name") + /// let request = Player.select(idColumn, nameColumn) + /// let row = try Row.fetchOne(db, request) + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchOne(_ db: Database, _ request: Request) throws -> Row? { + let (statement, adapter) = try request.prepare(db) + return try fetchOne(statement, adapter: adapter) + } +} + +extension Row { + + // MARK: - Fetching From SQL + + /// Returns a cursor over rows fetched from an SQL query. + /// + /// let rows = try Row.fetchCursor(db, "SELECT id, name FROM players") // RowCursor + /// while let row = try rows.next() { // Row + /// let id: Int64 = row[0] + /// let name: String = row[1] + /// } + /// + /// Fetched rows are reused during the cursor iteration: don't turn a row + /// cursor into an array with `Array(rows)` or `rows.filter { ... }` since + /// you would not get the distinct rows you expect. Use `Row.fetchAll(...)` + /// instead. + /// + /// For the same reason, make sure you make a copy whenever you extract a + /// row for later use: `row.copy()`. + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: A cursor over fetched rows. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> RowCursor { + return try fetchCursor(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) + } + + /// Returns an array of rows fetched from an SQL query. + /// + /// let rows = try Row.fetchAll(db, "SELECT ...") + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An array of rows. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Row] { + return try fetchAll(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) + } + + /// Returns a single row fetched from an SQL query. + /// + /// let row = try Row.fetchOne(db, "SELECT ...") + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An optional row. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchOne(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Row? { + return try fetchOne(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) + } +} + +extension Row : ExpressibleByDictionaryLiteral { + + /// Creates a row initialized with elements. Column order is preserved, and + /// duplicated columns names are allowed. + /// + /// let row: Row = ["foo": 1, "foo": "bar", "baz": nil] + /// print(row) + /// // Prints <Row foo:1 foo:"bar" baz:NULL> + public convenience init(dictionaryLiteral elements: (String, DatabaseValueConvertible?)...) { + self.init(impl: ArrayRowImpl(columns: elements.map { ($0, $1?.databaseValue ?? .null) })) + } +} + +extension Row : Collection { + + // MARK: - Row as a Collection of (ColumnName, DatabaseValue) Pairs + + /// The index of the first (ColumnName, DatabaseValue) pair. + public var startIndex: RowIndex { + return Index(0) + } + + /// The "past-the-end" index, successor of the index of the last + /// (ColumnName, DatabaseValue) pair. + public var endIndex: RowIndex { + return Index(count) + } + + /// Accesses the (ColumnName, DatabaseValue) pair at given index. + public subscript(position: RowIndex) -> (String, DatabaseValue) { + let index = position.index + GRDBPrecondition(index >= 0 && index < count, "row index out of range") + return ( + impl.columnName(atUncheckedIndex: index), + impl.databaseValue(atUncheckedIndex: index)) + } + + /// Returns the position immediately after `i`. + /// + /// - Precondition: `(startIndex..<endIndex).contains(i)` + public func index(after i: RowIndex) -> RowIndex { + return RowIndex(i.index + 1) + } + + /// Replaces `i` with its successor. + public func formIndex(after i: inout RowIndex) { + i = RowIndex(i.index + 1) + } +} + +/// Row adopts Equatable. +extension Row : Equatable { + + /// Returns true if and only if both rows have the same columns and values, + /// in the same order. Columns are compared in a case-sensitive way. + public static func == (lhs: Row, rhs: Row) -> Bool { + if lhs === rhs { + return true + } + + guard lhs.count == rhs.count else { + return false + } + + var liter = lhs.makeIterator() + var riter = rhs.makeIterator() + + while let (lcol, lval) = liter.next(), let (rcol, rval) = riter.next() { + guard lcol == rcol else { + return false + } + guard lval == rval else { + return false + } + } + + let lscopeNames = lhs.impl.scopeNames + let rscopeNames = rhs.impl.scopeNames + guard lscopeNames == rscopeNames else { + return false + } + + for name in lscopeNames { + let lscope = lhs.scoped(on: name) + let rscope = rhs.scoped(on: name) + guard lscope == rscope else { + return false + } + } + + return true + } +} + +/// Row adopts Hashable. +extension Row : Hashable { + /// The hash value + public var hashValue: Int { + return columnNames.reduce(0) { (acc, column) in acc ^ column.hashValue } ^ + databaseValues.reduce(0) { (acc, dbValue) in acc ^ dbValue.hashValue } + } +} + +/// Row adopts CustomStringConvertible. +extension Row: CustomStringConvertible { + /// A textual representation of `self`. + public var description: String { + return "<Row" + + map { (column, dbValue) in + " \(column):\(dbValue)" + }.joined(separator: "") + + ">" + } +} + + +// MARK: - RowIndex + +/// Indexes to (columnName, dbValue) pairs in a database row. +public struct RowIndex : Comparable { + let index: Int + init(_ index: Int) { self.index = index } + + /// Equality operator + public static func == (lhs: RowIndex, rhs: RowIndex) -> Bool { + return lhs.index == rhs.index + } + + // Comparison operator + public static func < (lhs: RowIndex, rhs: RowIndex) -> Bool { + return lhs.index < rhs.index + } +} + + +// MARK: - RowImpl + +// The protocol for Row underlying implementation +protocol RowImpl { + var count: Int { get } + var isFetched: Bool { get } + func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue + func dataNoCopy(atUncheckedIndex index:Int) -> Data? + func columnName(atUncheckedIndex index: Int) -> String + + // This method MUST be case-insensitive, and returns the index of the + // leftmost column that matches *name*. + func index(ofColumn name: String) -> Int? + + func scoped(on name: String) -> Row? + var scopeNames: Set<String> { get } + + // row.impl is guaranteed to be self. + func copy(_ row: Row) -> Row +} + + +/// See Row.init(dictionary:) +private struct ArrayRowImpl : RowImpl { + let columns: [(String, DatabaseValue)] + + init(columns: [(String, DatabaseValue)]) { + self.columns = columns + } + + var count: Int { + return columns.count + } + + var isFetched: Bool { + return false + } + + func dataNoCopy(atUncheckedIndex index:Int) -> Data? { + return databaseValue(atUncheckedIndex: index).losslessConvert() + } + + func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue { + return columns[index].1 + } + + func columnName(atUncheckedIndex index: Int) -> String { + return columns[index].0 + } + + // This method MUST be case-insensitive, and returns the index of the + // leftmost column that matches *name*. + func index(ofColumn name: String) -> Int? { + let lowercaseName = name.lowercased() + return columns.index { (column, _) in column.lowercased() == lowercaseName } + } + + func scoped(on name: String) -> Row? { + return nil + } + + var scopeNames: Set<String> { + return [] + } + + func copy(_ row: Row) -> Row { + return row + } +} + + +/// See Row.init(copiedFromStatementRef:sqliteStatement:) +private struct StatementCopyRowImpl : RowImpl { + let dbValues: ContiguousArray<DatabaseValue> + let columnNames: [String] + + init(sqliteStatement: SQLiteStatement, columnNames: [String]) { + let sqliteStatement = sqliteStatement + self.dbValues = ContiguousArray((0..<sqlite3_column_count(sqliteStatement)).map { DatabaseValue(sqliteStatement: sqliteStatement, index: $0) } as [DatabaseValue]) + self.columnNames = columnNames + } + + var count: Int { + return columnNames.count + } + + var isFetched: Bool { + return true + } + + func dataNoCopy(atUncheckedIndex index:Int) -> Data? { + return databaseValue(atUncheckedIndex: index).losslessConvert() + } + + func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue { + return dbValues[index] + } + + func columnName(atUncheckedIndex index: Int) -> String { + return columnNames[index] + } + + // This method MUST be case-insensitive, and returns the index of the + // leftmost column that matches *name*. + func index(ofColumn name: String) -> Int? { + let lowercaseName = name.lowercased() + return columnNames.index { $0.lowercased() == lowercaseName } + } + + func scoped(on name: String) -> Row? { + return nil + } + + var scopeNames: Set<String> { + return [] + } + + func copy(_ row: Row) -> Row { + return row + } +} + + +/// See Row.init(statement:) +private struct StatementRowImpl : RowImpl { + let statementRef: Unmanaged<SelectStatement> + let sqliteStatement: SQLiteStatement + let lowercaseColumnIndexes: [String: Int] + + init(sqliteStatement: SQLiteStatement, statementRef: Unmanaged<SelectStatement>) { + self.statementRef = statementRef + self.sqliteStatement = sqliteStatement + // Optimize row["..."] + let lowercaseColumnNames = (0..<sqlite3_column_count(sqliteStatement)).map { String(cString: sqlite3_column_name(sqliteStatement, Int32($0))).lowercased() } + self.lowercaseColumnIndexes = Dictionary( + lowercaseColumnNames + .enumerated() + .map { ($0.element, $0.offset) }, + uniquingKeysWith: { (left, _) in left }) // keep leftmost indexes + } + + var count: Int { + return Int(sqlite3_column_count(sqliteStatement)) + } + + var isFetched: Bool { + return true + } + + func dataNoCopy(atUncheckedIndex index:Int) -> Data? { + guard sqlite3_column_type(sqliteStatement, Int32(index)) != SQLITE_NULL else { + return nil + } + guard let bytes = sqlite3_column_blob(sqliteStatement, Int32(index)) else { + return Data() + } + let count = Int(sqlite3_column_bytes(sqliteStatement, Int32(index))) + return Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: bytes), count: count, deallocator: .none) + } + + func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue { + return DatabaseValue(sqliteStatement: sqliteStatement, index: Int32(index)) + } + + func columnName(atUncheckedIndex index: Int) -> String { + return statementRef.takeUnretainedValue().columnNames[index] + } + + // This method MUST be case-insensitive, and returns the index of the + // leftmost column that matches *name*. + func index(ofColumn name: String) -> Int? { + if let index = lowercaseColumnIndexes[name] { + return index + } + return lowercaseColumnIndexes[name.lowercased()] + } + + func scoped(on name: String) -> Row? { + return nil + } + + var scopeNames: Set<String> { + return [] + } + + func copy(_ row: Row) -> Row { + return Row(copiedFromSQLiteStatement: sqliteStatement, statementRef: statementRef) + } +} + + +/// See Row.init() +private struct EmptyRowImpl : RowImpl { + var count: Int { + return 0 + } + + var isFetched: Bool { + return false + } + + func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue { + // Programmer error + fatalError("row index out of range") + } + + func dataNoCopy(atUncheckedIndex index:Int) -> Data? { + // Programmer error + fatalError("row index out of range") + } + + func columnName(atUncheckedIndex index: Int) -> String { + // Programmer error + fatalError("row index out of range") + } + + func index(ofColumn name: String) -> Int? { + return nil + } + + func scoped(on name: String) -> Row? { + return nil + } + + var scopeNames: Set<String> { + return [] + } + + func copy(_ row: Row) -> Row { + return row + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/RowAdapter.swift b/Pods/GRDB.swift/GRDB/Core/RowAdapter.swift new file mode 100644 index 0000000..16df6e3 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/RowAdapter.swift @@ -0,0 +1,423 @@ +import Foundation + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// LayoutedColumnMapping is a type that supports the RowAdapter protocol. +public struct LayoutedColumnMapping { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// An array of (baseIndex, mappedName) pairs, where baseIndex is the index + /// of a column in a base row, and mappedName the mapped name of + /// that column. + public let layoutColumns: [(Int, String)] + + /// A cache for layoutIndex(ofColumn:) + let lowercaseColumnIndexes: [String: Int] // [mappedColumn: layoutColumnIndex] + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Creates a LayoutedColumnMapping from an array of (baseIndex, mappedName) + /// pairs. In each pair: + /// + /// - baseIndex is the index of a column in a base row + /// - name is the mapped name of the column + /// + /// For example, the following LayoutedColumnMapping defines two columns, "foo" + /// and "bar", based on the base columns at indexes 1 and 2: + /// + /// LayoutedColumnMapping(layoutColumns: [(1, "foo"), (2, "bar")]) + /// + /// Use it in your custom RowAdapter type: + /// + /// struct FooBarAdapter : RowAdapter { + /// func layoutAdapter(layout: RowLayout) throws -> LayoutedRowAdapter { + /// return LayoutedColumnMapping(layoutColumns: [(1, "foo"), (2, "bar")]) + /// } + /// } + /// + /// // <Row foo:"foo" bar: "bar"> + /// try Row.fetchOne(db, "SELECT NULL, 'foo', 'bar'", adapter: FooBarAdapter()) + public init<S: Sequence>(layoutColumns: S) where S.Iterator.Element == (Int, String) { + self.layoutColumns = Array(layoutColumns) + self.lowercaseColumnIndexes = Dictionary( + layoutColumns + .enumerated() + .map { ($0.element.1.lowercased(), $0.offset) }, + uniquingKeysWith: { (left, _) in left }) // keep leftmost indexes + } + + func baseColumnIndex(atMappingIndex index: Int) -> Int { + return layoutColumns[index].0 + } + + func columnName(atMappingIndex index: Int) -> String { + return layoutColumns[index].1 + } +} + +/// LayoutedColumnMapping adopts LayoutedRowAdapter +extension LayoutedColumnMapping : LayoutedRowAdapter { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Returns self. + public var mapping: LayoutedColumnMapping { + return self + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Returns the empty dictionary. + public var scopes: [String: LayoutedRowAdapter] { + return [:] + } +} + +extension LayoutedColumnMapping : RowLayout { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Returns the index of the leftmost column named `name`, in a + /// case-insensitive way. + public func layoutIndex(ofColumn name: String) -> Int? { + if let index = lowercaseColumnIndexes[name] { + return index + } + return lowercaseColumnIndexes[name.lowercased()] + } +} + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// LayoutedRowAdapter is a protocol that supports the RowAdapter protocol. +/// +/// GRBD ships with a ready-made type that adopts this protocol: +/// LayoutedColumnMapping. +public protocol LayoutedRowAdapter { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// A LayoutedColumnMapping that defines how to map a column name to a + /// column in a base row. + var mapping: LayoutedColumnMapping { get } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// The layouted row adapters for each scope. + var scopes: [String: LayoutedRowAdapter] { get } +} + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// RowLayout is a protocol that supports the RowAdapter protocol. It describes +/// a layout of a base row. +public protocol RowLayout { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// An array of (baseIndex, name) pairs, where baseIndex is the index + /// of a column in a base row, and name the name of that column. + var layoutColumns: [(Int, String)] { get } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Returns the index of the leftmost column named `name`, in a + /// case-insensitive way. + func layoutIndex(ofColumn name: String) -> Int? +} + +extension SelectStatement : RowLayout { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public var layoutColumns: [(Int, String)] { + return Array(columnNames.enumerated()) + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func layoutIndex(ofColumn name: String) -> Int? { + return index(ofColumn: name) + } +} + +/// RowAdapter is a protocol that helps two incompatible row interfaces working +/// together. +/// +/// GRDB ships with four concrete types that adopt the RowAdapter protocol: +/// +/// - ColumnMapping: renames row columns +/// - SuffixRowAdapter: hides the first columns of a row +/// - RangeRowAdapter: only exposes a range of columns +/// - ScopeAdapter: groups several adapters together to define named scopes +/// +/// To use a row adapter, provide it to any method that fetches: +/// +/// let adapter = SuffixRowAdapter(fromIndex: 2) +/// let sql = "SELECT 1 AS foo, 2 AS bar, 3 AS baz" +/// +/// // <Row baz:3> +/// try Row.fetchOne(db, sql, adapter: adapter) +public protocol RowAdapter { + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// You never call this method directly. It is called for you whenever an + /// adapter has to be applied. + /// + /// The result is a value that adopts LayoutedRowAdapter, such as + /// LayoutedColumnMapping. + /// + /// For example: + /// + /// // An adapter that turns any row to a row that contains a single + /// // column named "foo" whose value is the leftmost value of the + /// // base row. + /// struct FirstColumnAdapter : RowAdapter { + /// func layoutedAdapter(from layout: RowLayout) throws -> LayoutedRowAdapter { + /// return LayoutedColumnMapping(layoutColumns: [(0, "foo")]) + /// } + /// } + /// + /// // <Row foo:1> + /// try Row.fetchOne(db, "SELECT 1, 2, 3", adapter: FirstColumnAdapter()) + func layoutedAdapter(from layout: RowLayout) throws -> LayoutedRowAdapter +} + +extension RowAdapter { + /// Returns an adapter based on self, with added scopes. + /// + /// If self already defines scopes, the added scopes replace + /// eventual existing scopes with the same name. + /// + /// - parameter scopes: A dictionary that maps scope names to + /// row adapters. + public func addingScopes(_ scopes: [String: RowAdapter]) -> RowAdapter { + return ScopeAdapter(mainAdapter: self, scopes: scopes) + } +} + +extension RowAdapter { + func baseColumnIndex(atIndex index: Int, layout: RowLayout) throws -> Int { + return try layoutedAdapter(from: layout).mapping.baseColumnIndex(atMappingIndex: index) + } +} + +/// ColumnMapping is a row adapter that maps column names. +/// +/// let adapter = ColumnMapping(["foo": "bar"]) +/// let sql = "SELECT 'foo' AS foo, 'bar' AS bar, 'baz' AS baz" +/// +/// // <Row foo:"bar"> +/// try Row.fetchOne(db, sql, adapter: adapter) +public struct ColumnMapping : RowAdapter { + /// A dictionary from mapped column names to column names in a base row. + let mapping: [String: String] + + /// Creates a ColumnMapping with a dictionary from mapped column names to + /// column names in a base row. + public init(_ mapping: [String: String]) { + self.mapping = mapping + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func layoutedAdapter(from layout: RowLayout) throws -> LayoutedRowAdapter { + let layoutColumns = try mapping + .map { (mappedColumn, baseColumn) -> (Int, String) in + guard let index = layout.layoutIndex(ofColumn: baseColumn) else { + let columnNames = layout.layoutColumns.map { $0.1 } + throw DatabaseError(resultCode: .SQLITE_MISUSE, message: "Mapping references missing column \(baseColumn). Valid column names are: \(columnNames.joined(separator: ", ")).") + } + let baseIndex = layout.layoutColumns[index].0 + return (baseIndex, mappedColumn) + } + .sorted { $0.0 < $1.0 } // preserve ordering of base columns + return LayoutedColumnMapping(layoutColumns: layoutColumns) + } +} + +/// SuffixRowAdapter is a row adapter that hides the first columns in a row. +/// +/// let adapter = SuffixRowAdapter(fromIndex: 2) +/// let sql = "SELECT 1 AS foo, 2 AS bar, 3 AS baz" +/// +/// // <Row baz:3> +/// try Row.fetchOne(db, sql, adapter: adapter) +public struct SuffixRowAdapter : RowAdapter { + /// The suffix index + let index: Int + + /// Creates a SuffixRowAdapter that hides all columns before the + /// provided index. + /// + /// If index is 0, the layout row is identical to the base row. + public init(fromIndex index: Int) { + GRDBPrecondition(index >= 0, "Negative column index is out of range") + self.index = index + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func layoutedAdapter(from layout: RowLayout) throws -> LayoutedRowAdapter { + return LayoutedColumnMapping(layoutColumns: layout.layoutColumns.suffix(from: index)) + } +} + +/// RangeRowAdapter is a row adapter that only exposes a range of columns. +/// +/// let adapter = RangeRowAdapter(1..<3) +/// let sql = "SELECT 1 AS foo, 2 AS bar, 3 AS baz, 4 as qux" +/// +/// // <Row bar:2 baz: 3> +/// try Row.fetchOne(db, sql, adapter: adapter) +public struct RangeRowAdapter : RowAdapter { + /// The range + let range: CountableRange<Int> + + /// Creates a RangeRowAdapter that only exposes a range of columns. + public init(_ range: CountableRange<Int>) { + GRDBPrecondition(range.lowerBound >= 0, "Negative column index is out of range") + self.range = range + } + + /// Creates a RangeRowAdapter that only exposes a range of columns. + public init(_ range: CountableClosedRange<Int>) { + GRDBPrecondition(range.lowerBound >= 0, "Negative column index is out of range") + self.range = range.lowerBound..<(range.upperBound + 1) + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func layoutedAdapter(from layout: RowLayout) throws -> LayoutedRowAdapter { + return LayoutedColumnMapping(layoutColumns: layout.layoutColumns[range]) + } +} + +/// ScopeAdapter is a row adapter that lets you define scopes on rows. +/// +/// // Two adapters +/// let fooAdapter = ColumnMapping(["value": "foo"]) +/// let barAdapter = ColumnMapping(["value": "bar"]) +/// +/// // Define scopes +/// let adapter = ScopeAdapter([ +/// "foo": fooAdapter, +/// "bar": barAdapter]) +/// +/// // Fetch +/// let sql = "SELECT 'foo' AS foo, 'bar' AS bar" +/// let row = try Row.fetchOne(db, sql, adapter: adapter)! +/// +/// // Scoped rows: +/// if let fooRow = row.scoped(on: "foo") { +/// fooRow["value"] // "foo" +/// } +/// if let barRow = row.scopeed(on: "bar") { +/// barRow["value"] // "bar" +/// } +public struct ScopeAdapter : RowAdapter { + + /// The main adapter + let mainAdapter: RowAdapter + + /// The scope adapters + let scopes: [String: RowAdapter] + + /// Creates a scoped adapter. + /// + /// - parameter scopes: A dictionary that maps scope names to + /// row adapters. + public init(_ scopes: [String: RowAdapter]) { + self.mainAdapter = SuffixRowAdapter(fromIndex: 0) // Use SuffixRowAdapter(fromIndex: 0) as the identity adapter + self.scopes = scopes + } + + init(mainAdapter: RowAdapter, scopes: [String: RowAdapter]) { + self.mainAdapter = mainAdapter + self.scopes = scopes + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func layoutedAdapter(from layout: RowLayout) throws -> LayoutedRowAdapter { + let layoutedAdapter = try mainAdapter.layoutedAdapter(from: layout) + var layoutedScopes = layoutedAdapter.scopes + for (name, adapter) in scopes { + try layoutedScopes[name] = adapter.layoutedAdapter(from: layout) + } + return LayoutedScopeAdapter( + mapping: layoutedAdapter.mapping, + scopes: layoutedScopes) + } +} + +/// The LayoutedRowAdapter for ScopeAdapter +struct LayoutedScopeAdapter : LayoutedRowAdapter { + let mapping: LayoutedColumnMapping + let scopes: [String: LayoutedRowAdapter] +} + +struct ChainedAdapter : RowAdapter { + let first: RowAdapter + let second: RowAdapter + + func layoutedAdapter(from layout: RowLayout) throws -> LayoutedRowAdapter { + return try second.layoutedAdapter(from: first.layoutedAdapter(from: layout).mapping) + } +} + +extension Row { + /// Creates a row from a base row and a statement adapter + convenience init(base: Row, adapter: LayoutedRowAdapter) { + self.init(impl: AdapterRowImpl(base: base, adapter: adapter)) + } + + /// Returns self if adapter is nil + func adapted(with adapter: RowAdapter?, layout: RowLayout) throws -> Row { + guard let adapter = adapter else { + return self + } + return try Row(base: self, adapter: adapter.layoutedAdapter(from: layout)) + } +} + +struct AdapterRowImpl : RowImpl { + let base: Row + let adapter: LayoutedRowAdapter + let mapping: LayoutedColumnMapping + + init(base: Row, adapter: LayoutedRowAdapter) { + self.base = base + self.adapter = adapter + self.mapping = adapter.mapping + } + + var count: Int { + return mapping.layoutColumns.count + } + + var isFetched: Bool { + return base.isFetched + } + + func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue { + return base[mapping.baseColumnIndex(atMappingIndex: index)] + } + + func dataNoCopy(atUncheckedIndex index:Int) -> Data? { + return base.dataNoCopy(atIndex: mapping.baseColumnIndex(atMappingIndex: index)) + } + + func columnName(atUncheckedIndex index: Int) -> String { + return mapping.columnName(atMappingIndex: index) + } + + func index(ofColumn name: String) -> Int? { + return mapping.layoutIndex(ofColumn: name) + } + + func scoped(on name: String) -> Row? { + guard let adapter = adapter.scopes[name] else { + return nil + } + return Row(base: base, adapter: adapter) + } + + var scopeNames: Set<String> { + return Set(adapter.scopes.keys) + } + + func copy(_ row: Row) -> Row { + return Row(base: base.copy(), adapter: adapter) + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/SchedulingWatchdog.swift b/Pods/GRDB.swift/GRDB/Core/SchedulingWatchdog.swift new file mode 100644 index 0000000..098fc32 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/SchedulingWatchdog.swift @@ -0,0 +1,62 @@ +import Dispatch + +/// SchedulingWatchdog makes sure that databases connections are used on correct +/// dispatch queues, and warns the user with a fatal error whenever she misuses +/// a database connection. +/// +/// Generally speaking, each connection has its own dispatch queue. But it's not +/// enough: users need to use two database connections at the same time: +/// https://github.com/groue/GRDB.swift/issues/55. To support this use case, a +/// single dispatch queue can be temporarily shared by two or more connections. +/// +/// - SchedulingWatchdog.makeSerializedQueue(allowingDatabase:) creates a +/// dispatch queue that allows one database. +/// +/// It does so by registering one instance of SchedulingWatchdog as a specific +/// of the dispatch queue, a SchedulingWatchdog that allows that database only. +/// +/// Later on, the queue can be shared by several databases with the method +/// allowing(databases:execute:). See SerializedDatabase.sync() for +/// an example. +/// +/// - preconditionValidQueue() crashes whenever a database is used in an invalid +/// dispatch queue. +final class SchedulingWatchdog { + private static let specificKey = DispatchSpecificKey<SchedulingWatchdog>() + private(set) var allowedDatabases: [Database] + + private init(allowedDatabase database: Database) { + allowedDatabases = [database] + } + + static func makeSerializedQueue(allowingDatabase database: Database) -> DispatchQueue { + let queue = DispatchQueue(label: "GRDB.SerializedDatabase") + let watchdog = SchedulingWatchdog(allowedDatabase: database) + queue.setSpecific(key: specificKey, value: watchdog) + return queue + } + + // Temporarily allows `databases` while executing `body` + func allowing<T>(databases: [Database], execute body: () throws -> T) rethrows -> T { + let backup = allowedDatabases + allowedDatabases.append(contentsOf: databases) + defer { allowedDatabases = backup } + return try body() + } + + static func preconditionValidQueue(_ db: Database, _ message: @autoclosure() -> String = "Database was not used on the correct thread.", file: StaticString = #file, line: UInt = #line) { + GRDBPrecondition(allows(db), message, file: file, line: line) + } + + static func allows(_ db: Database) -> Bool { + return current?.allows(db) ?? false + } + + func allows(_ db: Database) -> Bool { + return allowedDatabases.contains { $0 === db } + } + + static var current: SchedulingWatchdog? { + return DispatchQueue.getSpecific(key: specificKey) + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/SerializedDatabase.swift b/Pods/GRDB.swift/GRDB/Core/SerializedDatabase.swift new file mode 100644 index 0000000..5bb5a31 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/SerializedDatabase.swift @@ -0,0 +1,169 @@ +import Foundation + +/// A class that serializes accesses to a database. +final class SerializedDatabase { + /// The database connection + private let db: Database + + /// The database configuration + var configuration: Configuration { + return db.configuration + } + + /// The path to the database file + var path: String + + /// The dispatch queue + private let queue: DispatchQueue + + init(path: String, configuration: Configuration = Configuration(), schemaCache: DatabaseSchemaCache) throws { + // According to https://www.sqlite.org/threadsafe.html + // + // > SQLite support three different threading modes: + // > + // > 1. Multi-thread. In this mode, SQLite can be safely used by + // > multiple threads provided that no single database connection is + // > used simultaneously in two or more threads. + // > + // > 2. Serialized. In serialized mode, SQLite can be safely used by + // > multiple threads with no restriction. + // > + // > [...] + // > + // > The default mode is serialized. + // + // Since our database connection is only used via our serial dispatch + // queue, there is no purpose using the default serialized mode. + var config = configuration + config.threadingMode = .multiThread + + db = try Database(path: path, configuration: config, schemaCache: schemaCache) + queue = SchedulingWatchdog.makeSerializedQueue(allowingDatabase: db) + self.path = path + try queue.sync { + try db.setup() + } + } + + deinit { + // Database may be deallocated in its own queue: allow reentrancy + reentrantSync { db in + db.close() + } + } + + /// Synchronously executes a block the serialized dispatch queue, and + /// returns its result. + /// + /// This method is *not* reentrant. + func sync<T>(_ block: (Database) throws -> T) rethrows -> T { + // Three different cases: + // + // 1. A database is invoked from some queue like the main queue: + // + // dbQueue.inDatabase { db in // <-- we're here + // } + // + // 2. A database is invoked in a reentrant way: + // + // dbQueue.inDatabase { db in + // dbQueue.inDatabase { db in // <-- we're here + // } + // } + // + // 3. A database in invoked from another database: + // + // dbQueue1.inDatabase { db1 in + // dbQueue2.inDatabase { db2 in // <-- we're here + // } + // } + + guard let watchdog = SchedulingWatchdog.current else { + // Case 1 + return try queue.sync { + try block(db) + } + } + + // Case 2 is forbidden. + GRDBPrecondition(!watchdog.allows(db), "Database methods are not reentrant.") + + // Case 3 + return try queue.sync { + try SchedulingWatchdog.current!.allowing(databases: watchdog.allowedDatabases) { + try block(db) + } + } + } + + /// Synchronously executes a block the serialized dispatch queue, and + /// returns its result. + /// + /// This method is reentrant. + func reentrantSync<T>(_ block: (Database) throws -> T) rethrows -> T { + // Three different cases: + // + // 1. A database is invoked from some queue like the main queue: + // + // dbQueue.inDatabase { db in // <-- we're here + // } + // + // 2. A database is invoked in a reentrant way: + // + // dbQueue.inDatabase { db in + // dbQueue.inDatabase { db in // <-- we're here + // } + // } + // + // 3. A database in invoked from another database: + // + // dbQueue1.inDatabase { db1 in + // dbQueue2.inDatabase { db2 in // <-- we're here + // } + // } + + guard let watchdog = SchedulingWatchdog.current else { + // Case 1 + return try queue.sync { + try block(db) + } + } + + // Case 2 + if watchdog.allows(db) { + return try block(db) + } + + // Case 3 + return try queue.sync { + try SchedulingWatchdog.current!.allowing(databases: watchdog.allowedDatabases) { + try block(db) + } + } + } + + /// Asynchronously executes a block in the serialized dispatch queue. + func async(_ block: @escaping (Database) -> Void) { + queue.async { + block(self.db) + } + } + + /// Returns true if any only if the current dispatch queue is valid. + var onValidQueue: Bool { + return SchedulingWatchdog.allows(db) + } + + /// Executes the block in the current queue. + /// + /// - precondition: the current dispatch queue is valid. + func execute<T>(_ block: (Database) throws -> T) rethrows -> T { + preconditionValidQueue() + return try block(db) + } + + /// Fatal error if current dispatch queue is not valid. + func preconditionValidQueue(_ message: @autoclosure() -> String = "Database was not used on the correct thread.", file: StaticString = #file, line: UInt = #line) { + SchedulingWatchdog.preconditionValidQueue(db, message, file: file, line: line) + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/Statement.swift b/Pods/GRDB.swift/GRDB/Core/Statement.swift new file mode 100644 index 0000000..9a99f51 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Statement.swift @@ -0,0 +1,947 @@ +import Foundation +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif + +/// A raw SQLite statement, suitable for the SQLite C API. +public typealias SQLiteStatement = OpaquePointer + +/// A statement represents an SQL query. +/// +/// It is the base class of UpdateStatement that executes *update statements*, +/// and SelectStatement that fetches rows. +public class Statement { + + /// The raw SQLite statement, suitable for the SQLite C API. + public let sqliteStatement: SQLiteStatement + + /// The SQL query + public var sql: String { + // trim white space and semicolumn for homogeneous output + return String(cString: sqlite3_sql(sqliteStatement)) + .trimmingCharacters(in: CharacterSet(charactersIn: ";").union(.whitespacesAndNewlines)) + } + + /// The database + unowned let database: Database + + init(database: Database, sqliteStatement: SQLiteStatement) { + self.database = database + self.sqliteStatement = sqliteStatement + } + + /// Creates a prepared statement. + /// + /// - parameter database: A database connection. + /// - parameter sql: An SQL query. + /// - parameter prepFlags: Flags for sqlite3_prepare_v3 (available from + /// SQLite 3.20.0, see http://www.sqlite.org/c3ref/prepare.html) + /// - parameter observer: A StatementCompilationObserver + fileprivate init(database: Database, sql: String, prepFlags: Int32, observer: StatementCompilationObserver) throws { + SchedulingWatchdog.preconditionValidQueue(database) + + observer.start() + defer { observer.stop() } + + let sqlCodeUnits = sql.utf8CString + var sqliteStatement: SQLiteStatement? = nil + var code: Int32 = 0 + var remainingSQL = "" + sqlCodeUnits.withUnsafeBufferPointer { codeUnits in + let sqlStart = UnsafePointer<Int8>(codeUnits.baseAddress)! + var sqlEnd: UnsafePointer<Int8>? = nil + // sqlite3_prepare_v3 was introduced in SQLite 3.20.0 http://www.sqlite.org/changes.html#version_3_20 + #if GRDBCUSTOMSQLITE + code = sqlite3_prepare_v3(database.sqliteConnection, sqlStart, -1, UInt32(bitPattern: prepFlags), &sqliteStatement, &sqlEnd) + #else + code = sqlite3_prepare_v2(database.sqliteConnection, sqlStart, -1, &sqliteStatement, &sqlEnd) + #endif + let remainingData = Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: sqlEnd!), count: sqlStart + sqlCodeUnits.count - sqlEnd! - 1, deallocator: .none) + remainingSQL = String(data: remainingData, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) + } + + guard code == SQLITE_OK else { + throw DatabaseError(resultCode: code, message: database.lastErrorMessage, sql: sql) + } + + guard remainingSQL.isEmpty else { + sqlite3_finalize(sqliteStatement) + throw DatabaseError(resultCode: .SQLITE_MISUSE, message: "Multiple statements found. To execute multiple statements, use Database.execute() instead.", sql: sql, arguments: nil) + } + + self.database = database + self.sqliteStatement = sqliteStatement! + + } + + deinit { + sqlite3_finalize(sqliteStatement) + } + + final func reset() { + // It looks like sqlite3_reset() does not access the file system. + // This function call should thus succeed, unless a GRDB bug, or a + // programmer error (reusing a failed statement): there is no point + // throwing any error. + let code = sqlite3_reset(sqliteStatement) + guard code == SQLITE_OK else { + fatalError(DatabaseError(resultCode: code, message: database.lastErrorMessage, sql: sql).description) + } + } + + + // MARK: Arguments + + var argumentsNeedValidation = true + var _arguments: StatementArguments = [] + + lazy var sqliteArgumentCount: Int = { + Int(sqlite3_bind_parameter_count(self.sqliteStatement)) + }() + + // Returns ["id", nil", "name"] for "INSERT INTO table VALUES (:id, ?, :name)" + fileprivate lazy var sqliteArgumentNames: [String?] = { + return (0..<self.sqliteArgumentCount).map { + guard let cString = sqlite3_bind_parameter_name(self.sqliteStatement, Int32($0 + 1)) else { + return nil + } + return String(String(cString: cString).characters.dropFirst()) // Drop initial ":", "@", "$" + } + }() + + /// The statement arguments. + public var arguments: StatementArguments { + get { return _arguments } + set { + // Force arguments validity: it is a programmer error to provide + // arguments that do not match the statement. + try! setArgumentsWithValidation(newValue) + } + } + + /// Throws a DatabaseError of code SQLITE_ERROR if arguments don't fill all + /// statement arguments. + public func validate(arguments: StatementArguments) throws { + var arguments = arguments + _ = try arguments.consume(self, allowingRemainingValues: false) + } + + /// Set arguments without any validation. Trades safety for performance. + public func unsafeSetArguments(_ arguments: StatementArguments) { + _arguments = arguments + argumentsNeedValidation = false + + reset() + clearBindings() + + var valuesIterator = arguments.values.makeIterator() + for (index, argumentName) in sqliteArgumentNames.enumerated() { + if let argumentName = argumentName, let value = arguments.namedValues[argumentName] { + bind(value, at: index) + } else if let value = valuesIterator.next() { + bind(value, at: index) + } else { + bind(.null, at: index) + } + } + } + + func setArgumentsWithValidation(_ arguments: StatementArguments) throws { + // Validate + _arguments = arguments + var arguments = arguments + let bindings = try arguments.consume(self, allowingRemainingValues: false) + argumentsNeedValidation = false + + // Apply + reset() + clearBindings() + for (index, dbValue) in bindings.enumerated() { + bind(dbValue, at: index) + } + } + + // 0-based index + private func bind(_ dbValue: DatabaseValue, at index: Int) { + let code: Int32 + switch dbValue.storage { + case .null: + code = sqlite3_bind_null(sqliteStatement, Int32(index + 1)) + case .int64(let int64): + code = sqlite3_bind_int64(sqliteStatement, Int32(index + 1), int64) + case .double(let double): + code = sqlite3_bind_double(sqliteStatement, Int32(index + 1), double) + case .string(let string): + code = sqlite3_bind_text(sqliteStatement, Int32(index + 1), string, -1, SQLITE_TRANSIENT) + case .blob(let data): + code = data.withUnsafeBytes { bytes in + sqlite3_bind_blob(sqliteStatement, Int32(index + 1), bytes, Int32(data.count), SQLITE_TRANSIENT) + } + } + + // It looks like sqlite3_bind_xxx() functions do not access the file system. + // They should thus succeed, unless a GRDB bug: there is no point throwing any error. + guard code == SQLITE_OK else { + fatalError(DatabaseError(resultCode: code, message: database.lastErrorMessage, sql: sql).description) + } + } + + // Don't make this one public unless we keep the arguments property in sync. + private func clearBindings() { + // It looks like sqlite3_clear_bindings() does not access the file system. + // This function call should thus succeed, unless a GRDB bug: there is + // no point throwing any error. + let code = sqlite3_clear_bindings(sqliteStatement) + guard code == SQLITE_OK else { + fatalError(DatabaseError(resultCode: code, message: database.lastErrorMessage, sql: sql).description) + } + } + + fileprivate func prepare(withArguments arguments: StatementArguments?) { + // Force arguments validity: it is a programmer error to provide + // arguments that do not match the statement. + if let arguments = arguments { + try! setArgumentsWithValidation(arguments) + } else if argumentsNeedValidation { + try! validate(arguments: self.arguments) + } + } + + /// Utility function for cursors + func cursorReset(arguments: StatementArguments? = nil) { + SchedulingWatchdog.preconditionValidQueue(database) + prepare(withArguments: arguments) + reset() + } +} + + +// MARK: - SelectStatement + +/// A subclass of Statement that fetches database rows. +/// +/// You create SelectStatement with the Database.makeSelectStatement() method: +/// +/// try dbQueue.inDatabase { db in +/// let statement = try db.makeSelectStatement("SELECT COUNT(*) FROM players WHERE score > ?") +/// let moreThanTwentyCount = try Int.fetchOne(statement, arguments: [20])! +/// let moreThanThirtyCount = try Int.fetchOne(statement, arguments: [30])! +/// } +public final class SelectStatement : Statement { + /// Information about the table and columns read by a SelectStatement + public private(set) var selectionInfo: SelectionInfo + + /// Creates a prepared statement. + /// + /// - parameter database: A database connection. + /// - parameter sql: An SQL query. + /// - parameter prepFlags: Flags for sqlite3_prepare_v3 (available from + /// SQLite 3.20.0, see http://www.sqlite.org/c3ref/prepare.html) + init(database: Database, sql: String, prepFlags: Int32) throws { + self.selectionInfo = SelectionInfo() + let observer = StatementCompilationObserver(database) + try super.init(database: database, sql: sql, prepFlags: prepFlags, observer: observer) + Database.preconditionValidSelectStatement(sql: sql, observer: observer) + self.selectionInfo = observer.selectionInfo + } + + /// The number of columns in the resulting rows. + public lazy var columnCount: Int = { + Int(sqlite3_column_count(self.sqliteStatement)) + }() + + /// The column names, ordered from left to right. + public lazy var columnNames: [String] = { + let sqliteStatement = self.sqliteStatement + return (0..<self.columnCount).map { (index: Int) -> String in String(cString: sqlite3_column_name(sqliteStatement, Int32(index))) } + }() + + /// Cache for indexOfColumn(). Keys are lowercase. + private lazy var columnIndexes: [String: Int] = { + return Dictionary( + self.columnNames.enumerated().map { ($0.element.lowercased(), $0.offset) }, + uniquingKeysWith: { (left, _) in left }) // keep leftmost indexes + }() + + /// Returns the index of the leftmost column named `name`, in a + /// case-insensitive way. + public func index(ofColumn name: String) -> Int? { + return columnIndexes[name.lowercased()] + } + + /// Creates a cursor over the statement. This cursor does not produce any + /// value, and is only intended to give access to the sqlite3_step() + /// low-level function. + func cursor(arguments: StatementArguments? = nil) -> StatementCursor { + return StatementCursor(statement: self, arguments: arguments) + } + + /// Information about the table and columns read by a SelectStatement + public struct SelectionInfo : CustomStringConvertible { + mutating func insert(allColumnsOfTable table: String) { + tables.insert(table) + } + + mutating func insert(column: String, ofTable table: String) { + columns[table, default: []].insert(column) + } + + /// Returns true if isUnknown is true + func contains(anyColumnFrom table: String) -> Bool { + if isUnknown { return true } + return tables.contains(table) || columns.index(forKey: table) != nil + } + + /// Returns true if isUnknown is true + func contains(anyColumnIn columns: Set<String>, from table: String) -> Bool { + if isUnknown { return true } + return tables.contains(table) || !(self.columns[table]?.isDisjoint(with: columns) ?? true) + } + + init() { + self.init(isUnknown: false) + } + + static func unknown() -> SelectionInfo { + return self.init(isUnknown: true) + } + + /// If true, selection is unknown + private let isUnknown: Bool + + // `SELECT a, b FROM t1` -> ["t1": ["a", "b"]] + private var columns: [String: Set<String>] = [:] + + // `SELECT COUNT(*) FROM t1` -> ["t1"] + private var tables: Set<String> = [] + + private init(isUnknown: Bool) { + self.isUnknown = isUnknown + } + + /// A textual representation of `self`. + public var description: String { + if isUnknown { + return "unknown" + } + return tables.union(columns.keys) + .sorted() + .map { table -> String in + if let columns = columns[table] { + return "\(table)(\(columns.sorted().joined(separator: ",")))" + } else { + return "\(table)(*)" + } + } + .joined(separator: ",") + } + } +} + +/// A cursor that iterates a database statement without producing any value. +/// For example: +/// +/// try dbQueue.inDatabase { db in +/// let statement = db.makeSelectStatement("SELECT * FROM players") +/// let cursor: StatementCursor = statement.cursor() +/// } +public final class StatementCursor: Cursor { + public let statement: SelectStatement + private let sqliteStatement: SQLiteStatement + private var done = false + + // Use SelectStatement.cursor() instead + fileprivate init(statement: SelectStatement, arguments: StatementArguments? = nil) { + self.statement = statement + self.sqliteStatement = statement.sqliteStatement + statement.cursorReset(arguments: arguments) + } + + public func next() throws -> Void? { + if done { return nil } + switch sqlite3_step(sqliteStatement) { + case SQLITE_DONE: + done = true + return nil + case SQLITE_ROW: + return .some(()) + case let code: + statement.database.selectStatementDidFail(statement) + throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments) + } + } +} + + +// MARK: - UpdateStatement + +/// A subclass of Statement that executes SQL queries. +/// +/// You create UpdateStatement with the Database.makeUpdateStatement() method: +/// +/// try dbQueue.inTransaction { db in +/// let statement = try db.makeUpdateStatement("INSERT INTO players (name) VALUES (?)") +/// try statement.execute(arguments: ["Arthur"]) +/// try statement.execute(arguments: ["Barbara"]) +/// return .commit +/// } +public final class UpdateStatement : Statement { + enum TransactionStatementInfo { + enum SavepointAction : String { + case begin = "BEGIN" + case release = "RELEASE" + case rollback = "ROLLBACK" + } + + enum TransactionAction : String { + case begin = "BEGIN" + case commit = "COMMIT" + case rollback = "ROLLBACK" + } + + case transaction(action: TransactionAction) + case savepoint(name: String, action: SavepointAction) + } + + /// If true, the database schema cache gets invalidated after this statement + /// is executed. + private(set) var invalidatesDatabaseSchemaCache: Bool + private(set) var transactionStatementInfo: TransactionStatementInfo? + private(set) var databaseEventKinds: [DatabaseEventKind] + + init(database: Database, sqliteStatement: SQLiteStatement, invalidatesDatabaseSchemaCache: Bool, transactionStatementInfo: TransactionStatementInfo?, databaseEventKinds: [DatabaseEventKind]) { + self.invalidatesDatabaseSchemaCache = invalidatesDatabaseSchemaCache + self.transactionStatementInfo = transactionStatementInfo + self.databaseEventKinds = databaseEventKinds + super.init(database: database, sqliteStatement: sqliteStatement) + } + + /// Creates a prepared statement. + /// + /// - parameter database: A database connection. + /// - parameter sql: An SQL query. + /// - parameter prepFlags: Flags for sqlite3_prepare_v3 (available from + /// SQLite 3.20.0, see http://www.sqlite.org/c3ref/prepare.html) + init(database: Database, sql: String, prepFlags: Int32) throws { + self.invalidatesDatabaseSchemaCache = false + self.databaseEventKinds = [] + + let observer = StatementCompilationObserver(database) + try super.init(database: database, sql: sql, prepFlags: prepFlags, observer: observer) + self.invalidatesDatabaseSchemaCache = observer.invalidatesDatabaseSchemaCache + self.transactionStatementInfo = observer.transactionStatementInfo + self.databaseEventKinds = observer.databaseEventKinds + } + + /// Executes the SQL query. + /// + /// - parameter arguments: Statement arguments. + /// - throws: A DatabaseError whenever an SQLite error occurs. + public func execute(arguments: StatementArguments? = nil) throws { + SchedulingWatchdog.preconditionValidQueue(database) + prepare(withArguments: arguments) + reset() + database.updateStatementWillExecute(self) + + while true { + switch sqlite3_step(sqliteStatement) { + case SQLITE_ROW: + // The statement did return a row, and the user ignores the + // content of this row: + // + // try db.execute("SELECT ...") + // + // That's OK: maybe the selected rows perform side effects. + // For example: + // + // try db.execute("SELECT sqlcipher_export(...)") + // + // Or maybe the user doesn't know that the executed statement + // return rows (https://github.com/groue/GRDB.swift/issues/15); + // + // try db.execute("PRAGMA journal_mode=WAL") + // + // It is thus important that we consume *all* rows. + continue + + case SQLITE_DONE: + database.updateStatementDidExecute(self) + return + + case let code: + // Failure + // + // Let database rethrow eventual transaction observer error: + try database.updateStatementDidFail(self) + + throw DatabaseError(resultCode: code, message: database.lastErrorMessage, sql: sql, arguments: self.arguments) // Error uses self.arguments, not the optional arguments parameter. + } + } + } +} + + +// MARK: - StatementArguments + +/// StatementArguments provide values to argument placeholders in raw +/// SQL queries. +/// +/// Placeholders can take several forms (see https://www.sqlite.org/lang_expr.html#varparam +/// for more information): +/// +/// - `?NNN` (e.g. `?2`): the NNN-th argument (starts at 1) +/// - `?`: the N-th argument, where N is one greater than the largest argument +/// number already assigned +/// - `:AAAA` (e.g. `:name`): named argument +/// - `@AAAA` (e.g. `@name`): named argument +/// - `$AAAA` (e.g. `$name`): named argument +/// +/// ## Positional Arguments +/// +/// To fill question marks placeholders, feed StatementArguments with an array: +/// +/// db.execute( +/// "INSERT ... (?, ?)", +/// arguments: StatementArguments(["Arthur", 41])) +/// +/// // Array literals are automatically converted: +/// db.execute( +/// "INSERT ... (?, ?)", +/// arguments: ["Arthur", 41]) +/// +/// ## Named Arguments +/// +/// To fill named arguments, feed StatementArguments with a dictionary: +/// +/// db.execute( +/// "INSERT ... (:name, :score)", +/// arguments: StatementArguments(["name": "Arthur", "score": 41])) +/// +/// // Dictionary literals are automatically converted: +/// db.execute( +/// "INSERT ... (:name, :score)", +/// arguments: ["name": "Arthur", "score": 41]) +/// +/// ## Concatenating Arguments +/// +/// Several arguments can be concatenated and mixed with the +/// `append(contentsOf:)` method and the `+`, `&+`, `+=` operators: +/// +/// var arguments: StatementArguments = ["Arthur"] +/// arguments += [41] +/// db.execute("INSERT ... (?, ?)", arguments: arguments) +/// +/// `+` and `+=` operators consider that overriding named arguments is a +/// programmer error: +/// +/// var arguments: StatementArguments = ["name": "Arthur"] +/// arguments += ["name": "Barbara"] +/// // fatal error: already defined statement argument: name +/// +/// `&+` and `append(contentsOf:)` allow overriding named arguments: +/// +/// var arguments: StatementArguments = ["name": "Arthur"] +/// arguments = arguments &+ ["name": "Barbara"] +/// print(arguments) +/// // Prints ["name": "Barbara"] +/// +/// ## Mixed Arguments +/// +/// When a statement consumes a mix of named and positional arguments, it +/// prefers named arguments over positional ones. For example: +/// +/// let sql = "SELECT ?2 AS two, :foo AS foo, ?1 AS one, :foo AS foo2, :bar AS bar" +/// let row = try Row.fetchOne(db, sql, arguments: [1, 2, "bar"] + ["foo": "foo"])! +/// print(row) +/// // Prints <Row two:2 foo:"foo" one:1 foo2:"foo" bar:"bar"> +public struct StatementArguments { + + public var isEmpty: Bool { + return values.isEmpty && namedValues.isEmpty + } + + + // MARK: Empty Arguments + + /// Creates empty StatementArguments. + init() { + } + + // MARK: Positional Arguments + + /// Creates statement arguments from a sequence of optional values. + /// + /// let values: [DatabaseValueConvertible?] = ["foo", 1, nil] + /// db.execute("INSERT ... (?,?,?)", arguments: StatementArguments(values)) + /// + /// - parameter sequence: A sequence of DatabaseValueConvertible values. + /// - returns: A StatementArguments. + public init<Sequence: Swift.Sequence>(_ sequence: Sequence) where Sequence.Element == DatabaseValueConvertible? { + values = sequence.map { $0?.databaseValue ?? .null } + } + + /// Creates statement arguments from a sequence of optional values. + /// + /// let values: [String] = ["foo", "bar"] + /// db.execute("INSERT ... (?,?)", arguments: StatementArguments(values)) + /// + /// - parameter sequence: A sequence of DatabaseValueConvertible values. + /// - returns: A StatementArguments. + public init<Sequence: Swift.Sequence>(_ sequence: Sequence) where Sequence.Element: DatabaseValueConvertible { + values = sequence.map { $0.databaseValue } + } + + /// Creates statement arguments from any array. The result is nil unless all + /// array elements adopt DatabaseValueConvertible. + /// + /// - parameter array: An array + /// - returns: A StatementArguments. + public init?(_ array: [Any]) { + var values = [DatabaseValueConvertible?]() + for value in array { + guard let dbValue = DatabaseValue(value: value) else { + return nil + } + values.append(dbValue) + } + self.init(values) + } + + + // MARK: Named Arguments + + /// Creates statement arguments from a sequence of (key, value) dictionary, + /// such as a dictionary. + /// + /// let values: [String: DatabaseValueConvertible?] = ["firstName": nil, "lastName": "Miller"] + /// db.execute("INSERT ... (:firstName, :lastName)", arguments: StatementArguments(values)) + /// + /// - parameter sequence: A sequence of (key, value) pairs + /// - returns: A StatementArguments. + public init(_ dictionary: [String: DatabaseValueConvertible?]) { + namedValues = dictionary.mapValues { $0?.databaseValue ?? .null } + } + + /// Creates statement arguments from a sequence of (key, value) pairs, such + /// as a dictionary. + /// + /// let values: [String: DatabaseValueConvertible?] = ["firstName": nil, "lastName": "Miller"] + /// db.execute("INSERT ... (:firstName, :lastName)", arguments: StatementArguments(values)) + /// + /// - parameter sequence: A sequence of (key, value) pairs + /// - returns: A StatementArguments. + public init<Sequence: Swift.Sequence>(_ sequence: Sequence) where Sequence.Element == (String, DatabaseValueConvertible?) { + namedValues = Dictionary(uniqueKeysWithValues: sequence.map { ($0.0, $0.1?.databaseValue ?? .null) }) + } + + /// Creates statement arguments from [AnyHashable: Any]. + /// + /// The result is nil unless all dictionary keys are strings, and values + /// adopt DatabaseValueConvertible. + /// + /// - parameter dictionary: A dictionary. + /// - returns: A StatementArguments. + public init?(_ dictionary: [AnyHashable: Any]) { + var initDictionary = [String: DatabaseValueConvertible?]() + for (key, value) in dictionary { + guard let columnName = key as? String else { + return nil + } + guard let dbValue = DatabaseValue(value: value) else { + return nil + } + initDictionary[columnName] = dbValue + } + self.init(initDictionary) + } + + + // MARK: Adding arguments + + /// Extends statement arguments with other arguments. + /// + /// Positional arguments (provided as arrays) are concatenated: + /// + /// var arguments: StatementArguments = [1] + /// arguments.append(contentsOf: [2, 3]) + /// print(arguments) + /// // Prints [1, 2, 3] + /// + /// Named arguments (provided as dictionaries) are updated: + /// + /// var arguments: StatementArguments = ["foo": 1] + /// arguments.append(contentsOf: ["bar": 2]) + /// print(arguments) + /// // Prints ["foo": 1, "bar": 2] + /// + /// Arguments that were replaced, if any, are returned: + /// + /// var arguments: StatementArguments = ["foo": 1, "bar": 2] + /// let replacedValues = arguments.append(contentsOf: ["foo": 3]) + /// print(arguments) + /// // Prints ["foo": 3, "bar": 2] + /// print(replacedValues) + /// // Prints ["foo": 1] + /// + /// You can mix named and positional arguments (see documentation of + /// the StatementArguments type for more information about mixed arguments): + /// + /// var arguments: StatementArguments = ["foo": 1] + /// arguments.append(contentsOf: [2, 3]) + /// print(arguments) + /// // Prints ["foo": 1, 2, 3] + public mutating func append(contentsOf arguments: StatementArguments) -> [String: DatabaseValue] { + var replacedValues: [String: DatabaseValue] = [:] + values.append(contentsOf: arguments.values) + for (name, value) in arguments.namedValues { + if let replacedValue = namedValues.updateValue(value, forKey: name) { + replacedValues[name] = replacedValue + } + } + return replacedValues + } + + /// Creates a new StatementArguments by extending the left-hand size + /// arguments with the right-hand side arguments. + /// + /// Positional arguments (provided as arrays) are concatenated: + /// + /// let arguments: StatementArguments = [1] + [2, 3] + /// print(arguments) + /// // Prints [1, 2, 3] + /// + /// Named arguments (provided as dictionaries) are updated: + /// + /// let arguments: StatementArguments = ["foo": 1] + ["bar": 2] + /// print(arguments) + /// // Prints ["foo": 1, "bar": 2] + /// + /// You can mix named and positional arguments (see documentation of + /// the StatementArguments type for more information about mixed arguments): + /// + /// let arguments: StatementArguments = ["foo": 1] + [2, 3] + /// print(arguments) + /// // Prints ["foo": 1, 2, 3] + /// + /// If the arguments on the right-hand side has named parameters that are + /// already defined on the left, a fatal error is raised: + /// + /// let arguments: StatementArguments = ["foo": 1] + ["foo": 2] + /// // fatal error: already defined statement argument: foo + /// + /// This fatal error can be avoided with the &+ operator, or the + /// append(contentsOf:) method. + public static func + (lhs: StatementArguments, rhs: StatementArguments) -> StatementArguments { + var lhs = lhs + lhs += rhs + return lhs + } + + /// Creates a new StatementArguments by extending the left-hand size + /// arguments with the right-hand side arguments. + /// + /// Positional arguments (provided as arrays) are concatenated: + /// + /// let arguments: StatementArguments = [1] &+ [2, 3] + /// print(arguments) + /// // Prints [1, 2, 3] + /// + /// Named arguments (provided as dictionaries) are updated: + /// + /// let arguments: StatementArguments = ["foo": 1] &+ ["bar": 2] + /// print(arguments) + /// // Prints ["foo": 1, "bar": 2] + /// + /// You can mix named and positional arguments (see documentation of + /// the StatementArguments type for more information about mixed arguments): + /// + /// let arguments: StatementArguments = ["foo": 1] &+ [2, 3] + /// print(arguments) + /// // Prints ["foo": 1, 2, 3] + /// + /// If a named arguments is defined in both arguments, the right-hand + /// side wins: + /// + /// let arguments: StatementArguments = ["foo": 1] &+ ["foo": 2] + /// print(arguments) + /// // Prints ["foo": 2] + public static func &+ (lhs: StatementArguments, rhs: StatementArguments) -> StatementArguments { + var lhs = lhs + _ = lhs.append(contentsOf: rhs) + return lhs + } + + /// Extends the left-hand size arguments with the right-hand side arguments. + /// + /// Positional arguments (provided as arrays) are concatenated: + /// + /// var arguments: StatementArguments = [1] + /// arguments += [2, 3] + /// print(arguments) + /// // Prints [1, 2, 3] + /// + /// Named arguments (provided as dictionaries) are updated: + /// + /// var arguments: StatementArguments = ["foo": 1] + /// arguments += ["bar": 2] + /// print(arguments) + /// // Prints ["foo": 1, "bar": 2] + /// + /// You can mix named and positional arguments (see documentation of + /// the StatementArguments type for more information about mixed arguments): + /// + /// var arguments: StatementArguments = ["foo": 1] + /// arguments.append(contentsOf: [2, 3]) + /// print(arguments) + /// // Prints ["foo": 1, 2, 3] + /// + /// If the arguments on the right-hand side has named parameters that are + /// already defined on the left, a fatal error is raised: + /// + /// var arguments: StatementArguments = ["foo": 1] + /// arguments += ["foo": 2] + /// // fatal error: already defined statement argument: foo + /// + /// This fatal error can be avoided with the &+ operator, or the + /// append(contentsOf:) method. + public static func += (lhs: inout StatementArguments, rhs: StatementArguments) { + let replacedValues = lhs.append(contentsOf: rhs) + GRDBPrecondition(replacedValues.isEmpty, "already defined statement argument: \(replacedValues.keys.joined(separator: ", "))") + } + + + // MARK: Not Public + + var values: [DatabaseValue] = [] + var namedValues: [String: DatabaseValue] = [:] + + mutating func consume(_ statement: Statement, allowingRemainingValues: Bool) throws -> [DatabaseValue] { + let initialValuesCount = values.count + let bindings = try statement.sqliteArgumentNames.map { argumentName -> DatabaseValue in + if let argumentName = argumentName { + if let dbValue = namedValues[argumentName] { + return dbValue + } else if values.isEmpty { + throw DatabaseError(resultCode: .SQLITE_MISUSE, message: "missing statement argument: \(argumentName)", sql: statement.sql, arguments: nil) + } else { + return values.removeFirst() + } + } else { + if values.isEmpty { + throw DatabaseError(resultCode: .SQLITE_MISUSE, message: "wrong number of statement arguments: \(initialValuesCount)", sql: statement.sql, arguments: nil) + } else { + return values.removeFirst() + } + } + } + if !allowingRemainingValues && !values.isEmpty { + throw DatabaseError(resultCode: .SQLITE_MISUSE, message: "wrong number of statement arguments: \(initialValuesCount)", sql: statement.sql, arguments: nil) + } + return bindings + } +} + +extension StatementArguments : ExpressibleByArrayLiteral { + /// Returns a StatementArguments from an array literal: + /// + /// db.selectRows("SELECT ...", arguments: ["Arthur", 41]) + public init(arrayLiteral elements: DatabaseValueConvertible?...) { + self.init(elements) + } +} + +extension StatementArguments : ExpressibleByDictionaryLiteral { + /// Returns a StatementArguments from a dictionary literal: + /// + /// db.selectRows("SELECT ...", arguments: ["name": "Arthur", "score": 41]) + public init(dictionaryLiteral elements: (String, DatabaseValueConvertible?)...) { + self.init(elements) + } +} + +extension StatementArguments : CustomStringConvertible { + /// A textual representation of `self`. + public var description: String { + let valuesDescriptions = values.map { $0.description } + let namedValuesDescriptions = namedValues.map { (key, value) -> String in + return "\(String(reflecting: key)): \(value)" + } + return "[" + (namedValuesDescriptions + valuesDescriptions).joined(separator: ", ") + "]" + } +} + +/// A thread-unsafe statement cache +struct StatementCache { + unowned let db: Database + private var selectStatements: [String: SelectStatement] = [:] + private var updateStatements: [String: UpdateStatement] = [:] + + init(database: Database) { + self.db = database + } + + mutating func selectStatement(_ sql: String) throws -> SelectStatement { + if let statement = selectStatements[sql] { + return statement + } + + #if GRDBCUSTOMSQLITE + // http://www.sqlite.org/c3ref/c_prepare_persistent.html#sqlitepreparepersistent + // > The SQLITE_PREPARE_PERSISTENT flag is a hint to the query + // > planner that the prepared statement will be retained for a long + // > time and probably reused many times. + // + // This looks like a perfect match for cached statements. + // + // However SQLITE_PREPARE_PERSISTENT was only introduced in + // SQLite 3.20.0 http://www.sqlite.org/changes.html#version_3_20 + let statement = try db.makeSelectStatement(sql, prepFlags: SQLITE_PREPARE_PERSISTENT) + #else + let statement = try db.makeSelectStatement(sql) + #endif + selectStatements[sql] = statement + return statement + } + + mutating func updateStatement(_ sql: String) throws -> UpdateStatement { + if let statement = updateStatements[sql] { + return statement + } + + #if GRDBCUSTOMSQLITE + // http://www.sqlite.org/c3ref/c_prepare_persistent.html#sqlitepreparepersistent + // > The SQLITE_PREPARE_PERSISTENT flag is a hint to the query + // > planner that the prepared statement will be retained for a long + // > time and probably reused many times. + // + // This looks like a perfect match for cached statements. + // + // However SQLITE_PREPARE_PERSISTENT was only introduced in + // SQLite 3.20.0 http://www.sqlite.org/changes.html#version_3_20 + let statement = try db.makeUpdateStatement(sql, prepFlags: SQLITE_PREPARE_PERSISTENT) + #else + let statement = try db.makeUpdateStatement(sql) + #endif + updateStatements[sql] = statement + return statement + } + + mutating func clear() { + updateStatements = [:] + selectStatements = [:] + } + + mutating func remove(_ statement: SelectStatement) { + if let index = selectStatements.index(where: { $0.1 === statement }) { + selectStatements.remove(at: index) + } + } + + mutating func remove(_ statement: UpdateStatement) { + if let index = updateStatements.index(where: { $0.1 === statement }) { + updateStatements.remove(at: index) + } + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/StatementColumnConvertible.swift b/Pods/GRDB.swift/GRDB/Core/StatementColumnConvertible.swift new file mode 100644 index 0000000..418397a --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/StatementColumnConvertible.swift @@ -0,0 +1,459 @@ +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif + +/// The StatementColumnConvertible protocol grants access to the low-level C +/// interface that extracts values from query results: +/// https://www.sqlite.org/c3ref/column_blob.html. It can bring performance +/// improvements. +/// +/// To use it, have a value type adopt both StatementColumnConvertible and +/// DatabaseValueConvertible. GRDB will then automatically apply the +/// optimization whenever direct access to SQLite is possible: +/// +/// let rows = Row.fetchCursor(db, "SELECT ...") +/// while let row = try rows.next() { +/// let int: Int = row[0] // there +/// } +/// let ints = Int.fetchAll(db, "SELECT ...") // there +/// struct Player { +/// init(row: Row) { +/// name = row["name"] // there +/// score = row["score"] // there +/// } +/// } +/// +/// StatementColumnConvertible is already adopted by all Swift integer types, +/// Float, Double, String, and Bool. +public protocol StatementColumnConvertible { + + /// Initializes a value from a raw SQLite statement pointer. + /// + /// For example, here is the how Int64 adopts StatementColumnConvertible: + /// + /// extension Int64: StatementColumnConvertible { + /// init(sqliteStatement: SQLiteStatement, index: Int32) { + /// self = sqlite3_column_int64(sqliteStatement, index) + /// } + /// } + /// + /// This initializer is never called for NULL database values: don't perform + /// any extra check. + /// + /// See https://www.sqlite.org/c3ref/column_blob.html for more information. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. + init(sqliteStatement: SQLiteStatement, index: Int32) +} + +/// A cursor of database values extracted from a single column. +/// For example: +/// +/// try dbQueue.inDatabase { db in +/// let names: ColumnCursor<String> = try String.fetchCursor(db, "SELECT name FROM players") +/// while let name = names.next() { // String +/// print(name) +/// } +/// } +public final class ColumnCursor<Value: DatabaseValueConvertible & StatementColumnConvertible> : Cursor { + private let statement: SelectStatement + private let sqliteStatement: SQLiteStatement + private let columnIndex: Int32 + private var done = false + + init(statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws { + self.statement = statement + // We'll read from leftmost column at index 0, unless adapter mangles columns + self.columnIndex = try Int32(adapter?.baseColumnIndex(atIndex: 0, layout: statement) ?? 0) + self.sqliteStatement = statement.sqliteStatement + statement.cursorReset(arguments: arguments) + } + + public func next() throws -> Value? { + if done { return nil } + switch sqlite3_step(sqliteStatement) { + case SQLITE_DONE: + done = true + return nil + case SQLITE_ROW: + if sqlite3_column_type(sqliteStatement, columnIndex) == SQLITE_NULL { + var error = "could not convert database value NULL to \(Value.self) with statement `\(statement.sql)`" + if !statement.arguments.isEmpty { + error += " arguments \(statement.arguments)" + } + fatalError(error) + } + return Value(sqliteStatement: sqliteStatement, index: columnIndex) + case let code: + statement.database.selectStatementDidFail(statement) + throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments) + } + } +} + +/// A cursor of optional database values extracted from a single column. +/// For example: +/// +/// try dbQueue.inDatabase { db in +/// let emails: NullableColumnCursor<String> = try Optional<String>.fetchCursor(db, "SELECT email FROM players") +/// while let email = emails.next() { // String? +/// print(email ?? "<NULL>") +/// } +/// } +public final class NullableColumnCursor<Value: DatabaseValueConvertible & StatementColumnConvertible> : Cursor { + private let statement: SelectStatement + private let sqliteStatement: SQLiteStatement + private let columnIndex: Int32 + private var done = false + + init(statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws { + self.statement = statement + // We'll read from leftmost column at index 0, unless adapter mangles columns + self.columnIndex = try Int32(adapter?.baseColumnIndex(atIndex: 0, layout: statement) ?? 0) + self.sqliteStatement = statement.sqliteStatement + statement.cursorReset(arguments: arguments) + } + + public func next() throws -> Value?? { + if done { return nil } + switch sqlite3_step(sqliteStatement) { + case SQLITE_DONE: + done = true + return nil + case SQLITE_ROW: + if sqlite3_column_type(sqliteStatement, columnIndex) == SQLITE_NULL { + return .some(nil) + } + return Value(sqliteStatement: sqliteStatement, index: columnIndex) + case let code: + statement.database.selectStatementDidFail(statement) + throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments) + } + } +} + +/// Types that adopt both DatabaseValueConvertible and +/// StatementColumnConvertible can be efficiently initialized from +/// database values. +/// +/// See DatabaseValueConvertible for more information. +extension DatabaseValueConvertible where Self: StatementColumnConvertible { + + + // MARK: Fetching From SelectStatement + + /// Returns a cursor over values fetched from a prepared statement. + /// + /// let statement = try db.makeSelectStatement("SELECT name FROM ...") + /// let names = try String.fetchCursor(statement) // Cursor of String + /// while let name = try names.next() { // String + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - statement: The statement to run. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: A cursor over fetched values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> ColumnCursor<Self> { + return try ColumnCursor(statement: statement, arguments: arguments, adapter: adapter) + } + + /// Returns an array of values fetched from a prepared statement. + /// + /// let statement = try db.makeSelectStatement("SELECT name FROM ...") + /// let names = try String.fetchAll(statement) // [String] + /// + /// - parameters: + /// - statement: The statement to run. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An array of values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Self] { + return try Array(fetchCursor(statement, arguments: arguments, adapter: adapter)) + } + + /// Returns a single value fetched from a prepared statement. + /// + /// let statement = try db.makeSelectStatement("SELECT name FROM ...") + /// let name = try String.fetchOne(statement) // String? + /// + /// - parameters: + /// - statement: The statement to run. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An optional value. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchOne(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Self? { + // fetchOne returns nil if there is no row, or if there is a row with a null value + let cursor = try NullableColumnCursor<Self>(statement: statement, arguments: arguments, adapter: adapter) + return try cursor.next() ?? nil + } +} + +extension DatabaseValueConvertible where Self: StatementColumnConvertible { + + // MARK: Fetching From Request + + /// Returns a cursor over values fetched from a fetch request. + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn) + /// let names = try String.fetchCursor(db, request) // Cursor of String + /// while let name = try names.next() { // String + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - db: A database connection. + /// - request: A fetch request. + /// - returns: A cursor over fetched values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ db: Database, _ request: Request) throws -> ColumnCursor<Self> { + let (statement, adapter) = try request.prepare(db) + return try fetchCursor(statement, adapter: adapter) + } + + /// Returns an array of values fetched from a fetch request. + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn) + /// let names = try String.fetchAll(db, request) // [String] + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ db: Database, _ request: Request) throws -> [Self] { + let (statement, adapter) = try request.prepare(db) + return try fetchAll(statement, adapter: adapter) + } + + /// Returns a single value fetched from a fetch request. + /// + /// The result is nil if the query returns no row, or if no value can be + /// extracted from the first row. + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn) + /// let name = try String.fetchOne(db, request) // String? + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchOne(_ db: Database, _ request: Request) throws -> Self? { + let (statement, adapter) = try request.prepare(db) + return try fetchOne(statement, adapter: adapter) + } +} + +extension DatabaseValueConvertible where Self: StatementColumnConvertible { + + // MARK: Fetching From SQL + + /// Returns a cursor over values fetched from an SQL query. + /// + /// let names = try String.fetchCursor(db, "SELECT name FROM ...") // Cursor of String + /// while let name = try names.next() { // String + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: A cursor over fetched values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> ColumnCursor<Self> { + return try fetchCursor(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) + } + + /// Returns an array of values fetched from an SQL query. + /// + /// let names = try String.fetchAll(db, "SELECT name FROM ...") // [String] + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An array of values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Self] { + return try fetchAll(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) + } + + /// Returns a single value fetched from an SQL query. + /// + /// let name = try String.fetchOne(db, "SELECT name FROM ...") // String? + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An optional value. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchOne(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Self? { + return try fetchOne(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) + } +} + + +/// Swift's Optional comes with built-in methods that allow to fetch cursors +/// and arrays of optional DatabaseValueConvertible: +/// +/// try Optional<String>.fetchCursor(db, "SELECT name FROM ...", arguments:...) // Cursor of String? +/// try Optional<String>.fetchAll(db, "SELECT name FROM ...", arguments:...) // [String?] +/// +/// let statement = try db.makeSelectStatement("SELECT name FROM ...") +/// try Optional<String>.fetchCursor(statement, arguments:...) // Cursor of String? +/// try Optional<String>.fetchAll(statement, arguments:...) // [String?] +/// +/// DatabaseValueConvertible is adopted by Bool, Int, String, etc. +extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConvertible { + + // MARK: Fetching From SelectStatement + + /// Returns a cursor over optional values fetched from a prepared statement. + /// + /// let statement = try db.makeSelectStatement("SELECT name FROM ...") + /// let names = try Optional<String>.fetchCursor(statement) // Cursor of String? + /// while let name = try names.next() { // String? + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - statement: The statement to run. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: A cursor over fetched optional values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> NullableColumnCursor<Wrapped> { + return try NullableColumnCursor(statement: statement, arguments: arguments, adapter: adapter) + } + + /// Returns an array of optional values fetched from a prepared statement. + /// + /// let statement = try db.makeSelectStatement("SELECT name FROM ...") + /// let names = try Optional<String>.fetchAll(statement) // [String?] + /// + /// - parameters: + /// - statement: The statement to run. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An array of optional values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Wrapped?] { + return try Array(fetchCursor(statement, arguments: arguments, adapter: adapter)) + } +} + +extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConvertible { + + // MARK: Fetching From Request + + /// Returns a cursor over optional values fetched from a fetch request. + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn) + /// let names = try Optional<String>.fetchCursor(db, request) // Cursor of String? + /// while let name = try names.next() { // String? + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - db: A database connection. + /// - requet: A fetch request. + /// - returns: A cursor over fetched optional values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ db: Database, _ request: Request) throws -> NullableColumnCursor<Wrapped> { + let (statement, adapter) = try request.prepare(db) + return try fetchCursor(statement, adapter: adapter) + } + + /// Returns an array of optional values fetched from a fetch request. + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn) + /// let names = try Optional<String>.fetchAll(db, request) // [String?] + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ db: Database, _ request: Request) throws -> [Wrapped?] { + let (statement, adapter) = try request.prepare(db) + return try fetchAll(statement, adapter: adapter) + } +} + +extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConvertible { + + // MARK: Fetching From SQL + + /// Returns a cursor over optional values fetched from an SQL query. + /// + /// let names = try Optional<String>.fetchCursor(db, "SELECT name FROM ...") // Cursor of String? + /// while let name = try names.next() { // String? + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: A cursor over fetched optional values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> NullableColumnCursor<Wrapped> { + return try fetchCursor(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) + } + + /// Returns an array of optional values fetched from an SQL query. + /// + /// let names = try String.fetchAll(db, "SELECT name FROM ...") // [String?] + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - parameter arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An array of optional values. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Wrapped?] { + return try fetchAll(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/Support/CoreGraphics/CGFloat.swift b/Pods/GRDB.swift/GRDB/Core/Support/CoreGraphics/CGFloat.swift new file mode 100644 index 0000000..7359eb3 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Support/CoreGraphics/CGFloat.swift @@ -0,0 +1,18 @@ +import CoreGraphics + +/// CGFloat adopts DatabaseValueConvertible +extension CGFloat : DatabaseValueConvertible { + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return Double(self).databaseValue + } + + /// Returns a CGFloat initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> CGFloat? { + guard let double = Double.fromDatabaseValue(dbValue) else { + return nil + } + return CGFloat(double) + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/Support/Foundation/Data.swift b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/Data.swift new file mode 100644 index 0000000..c88608b --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/Data.swift @@ -0,0 +1,32 @@ +import Foundation +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif + +/// Data is convertible to and from DatabaseValue. +extension Data : DatabaseValueConvertible, StatementColumnConvertible { + public init(sqliteStatement: SQLiteStatement, index: Int32) { + if let bytes = sqlite3_column_blob(sqliteStatement, Int32(index)) { + let count = Int(sqlite3_column_bytes(sqliteStatement, Int32(index))) + self.init(bytes: bytes, count: count) // copy bytes + } else { + self.init() + } + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return DatabaseValue(storage: .blob(self)) + } + + /// Returns a Data initialized from *dbValue*, if it contains + /// a Blob. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Data? { + guard case .blob(let data) = dbValue.storage else { + return nil + } + return data + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift new file mode 100644 index 0000000..1e1385a --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift @@ -0,0 +1,288 @@ +import Foundation + +/// DatabaseDateComponents reads and stores DateComponents in the database. +public struct DatabaseDateComponents : DatabaseValueConvertible { + + /// The available formats for reading and storing date components. + public enum Format : String { + + /// The format "yyyy-MM-dd". + case YMD = "yyyy-MM-dd" + + /// The format "yyyy-MM-dd HH:mm". + /// + /// This format is lexically comparable with SQLite's CURRENT_TIMESTAMP. + case YMD_HM = "yyyy-MM-dd HH:mm" + + /// The format "yyyy-MM-dd HH:mm:ss". + /// + /// This format is lexically comparable with SQLite's CURRENT_TIMESTAMP. + case YMD_HMS = "yyyy-MM-dd HH:mm:ss" + + /// The format "yyyy-MM-dd HH:mm:ss.SSS". + /// + /// This format is lexically comparable with SQLite's CURRENT_TIMESTAMP. + case YMD_HMSS = "yyyy-MM-dd HH:mm:ss.SSS" + + /// The format "HH:mm". + case HM = "HH:mm" + + /// The format "HH:mm:ss". + case HMS = "HH:mm:ss" + + /// The format "HH:mm:ss.SSS". + case HMSS = "HH:mm:ss.SSS" + + var hasYMDComponents: Bool { + switch self { + case .YMD, .YMD_HM, .YMD_HMS, .YMD_HMSS: + return true + case .HM, .HMS, .HMSS: + return false + } + } + } + + // MARK: - NSDateComponents conversion + + /// The date components + public let dateComponents: DateComponents + + /// The database format + public let format: Format + + /// Creates a DatabaseDateComponents from a DateComponents and a format. + /// + /// The result is nil if and only if *dateComponents* is nil. + /// + /// - parameters: + /// - dateComponents: An optional DateComponents. + /// - format: The format used for storing the date components in + /// the database. + /// - returns: An optional DatabaseDateComponents. + public init?(_ dateComponents: DateComponents?, format: Format) { + guard let dateComponents = dateComponents else { + return nil + } + self.format = format + self.dateComponents = dateComponents + } + + + // MARK: - DatabaseValueConvertible adoption + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + let dateString: String? + switch format { + case .YMD_HM, .YMD_HMS, .YMD_HMSS, .YMD: + let year = dateComponents.year ?? 0 + let month = dateComponents.month ?? 1 + let day = dateComponents.day ?? 1 + dateString = NSString(format: "%04d-%02d-%02d", year, month, day) as String + default: + dateString = nil + } + + let timeString: String? + switch format { + case .YMD_HM, .HM: + let hour = dateComponents.hour ?? 0 + let minute = dateComponents.minute ?? 0 + timeString = NSString(format: "%02d:%02d", hour, minute) as String + case .YMD_HMS, .HMS: + let hour = dateComponents.hour ?? 0 + let minute = dateComponents.minute ?? 0 + let second = dateComponents.second ?? 0 + timeString = NSString(format: "%02d:%02d:%02d", hour, minute, second) as String + case .YMD_HMSS, .HMSS: + let hour = dateComponents.hour ?? 0 + let minute = dateComponents.minute ?? 0 + let second = dateComponents.second ?? 0 + let nanosecond = dateComponents.nanosecond ?? 0 + timeString = NSString(format: "%02d:%02d:%02d.%03d", hour, minute, second, Int(round(Double(nanosecond) / 1_000_000.0))) as String + default: + timeString = nil + } + + return [dateString, timeString].flatMap { $0 }.joined(separator: " ").databaseValue + } + + /// Returns a DatabaseDateComponents if *dbValue* contains a + /// valid date. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> DatabaseDateComponents? { + // https://www.sqlite.org/lang_datefunc.html + // + // Supported formats are: + // + // - YYYY-MM-DD + // - YYYY-MM-DD HH:MM + // - YYYY-MM-DD HH:MM:SS + // - YYYY-MM-DD HH:MM:SS.SSS + // - YYYY-MM-DDTHH:MM + // - YYYY-MM-DDTHH:MM:SS + // - YYYY-MM-DDTHH:MM:SS.SSS + // - HH:MM + // - HH:MM:SS + // - HH:MM:SS.SSS + + // We need a String + guard let string = String.fromDatabaseValue(dbValue) else { + return nil + } + + var dateComponents = DateComponents() + let scanner = Scanner(string: string) + scanner.charactersToBeSkipped = CharacterSet() + + let hasDate: Bool + + // YYYY or HH + var initialNumber: Int = 0 + if !scanner.scanInt(&initialNumber) { + return nil + } + switch scanner.scanLocation { + case 2: + // HH + hasDate = false + + let hour = initialNumber + if hour >= 0 && hour <= 23 { + dateComponents.hour = hour + } else { + return nil + } + + case 4: + // YYYY + hasDate = true + + let year = initialNumber + if year >= 0 && year <= 9999 { + dateComponents.year = year + } else { + return nil + } + + // - + if !scanner.scanString("-", into: nil) { + return nil + } + + // MM + var month: Int = 0 + if scanner.scanInt(&month) && month >= 1 && month <= 12 { + dateComponents.month = month + } else { + return nil + } + + // - + if !scanner.scanString("-", into: nil) { + return nil + } + + // DD + var day: Int = 0 + if scanner.scanInt(&day) && day >= 1 && day <= 31 { + dateComponents.day = day + } else { + return nil + } + + // YYYY-MM-DD + if scanner.isAtEnd { + return DatabaseDateComponents(dateComponents, format: .YMD) + } + + // T/space + if !scanner.scanString("T", into: nil) && !scanner.scanString(" ", into: nil) { + return nil + } + + // HH + var hour: Int = 0 + if scanner.scanInt(&hour) && hour >= 0 && hour <= 23 { + dateComponents.hour = hour + } else { + return nil + } + + default: + return nil + } + + // : + if !scanner.scanString(":", into: nil) { + return nil + } + + // MM + var minute: Int = 0 + if scanner.scanInt(&minute) && minute >= 0 && minute <= 59 { + dateComponents.minute = minute + } else { + return nil + } + + // [YYYY-MM-DD] HH:MM + if scanner.isAtEnd { + if hasDate { + return DatabaseDateComponents(dateComponents, format: .YMD_HM) + } else { + return DatabaseDateComponents(dateComponents, format: .HM) + } + } + + // : + if !scanner.scanString(":", into: nil) { + return nil + } + + // SS + var second: Int = 0 + if scanner.scanInt(&second) && second >= 0 && second <= 59 { + dateComponents.second = second + } else { + return nil + } + + // [YYYY-MM-DD] HH:MM:SS + if scanner.isAtEnd { + if hasDate { + return DatabaseDateComponents(dateComponents, format: .YMD_HMS) + } else { + return DatabaseDateComponents(dateComponents, format: .HMS) + } + } + + // . + if !scanner.scanString(".", into: nil) { + return nil + } + + // SSS + var millisecondDigits: NSString? = nil + if scanner.scanCharacters(from: .decimalDigits, into: &millisecondDigits), var millisecondDigits = millisecondDigits { + if millisecondDigits.length > 3 { + millisecondDigits = NSString(string: millisecondDigits.substring(to: 3)) + } + dateComponents.nanosecond = millisecondDigits.integerValue * 1_000_000 + } else { + return nil + } + + // [YYYY-MM-DD] HH:MM:SS.SSS + if scanner.isAtEnd { + if hasDate { + return DatabaseDateComponents(dateComponents, format: .YMD_HMSS) + } else { + return DatabaseDateComponents(dateComponents, format: .HMSS) + } + } + + // Unknown format + return nil + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/Support/Foundation/DatabaseValueConvertible+ReferenceConvertible.swift b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/DatabaseValueConvertible+ReferenceConvertible.swift new file mode 100644 index 0000000..c5c8c6a --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/DatabaseValueConvertible+ReferenceConvertible.swift @@ -0,0 +1,41 @@ +import Foundation + +/// DatabaseValueConvertible is free for ReferenceConvertible types whose +/// ReferenceType is itself DatabaseValueConvertible. +/// +/// class FooReference { ... } +/// struct Foo : ReferenceConvertible { +/// typealias ReferenceType = FooReference +/// } +/// +/// // If the ReferenceType adopts DatabaseValueConvertible... +/// extension FooReference : DatabaseValueConvertible { ... } +/// +/// // ... then the ReferenceConvertible type can freely adopt DatabaseValueConvertible: +/// extension Foo : DatabaseValueConvertible { /* empty */ } +extension DatabaseValueConvertible where Self: ReferenceConvertible, Self.ReferenceType: DatabaseValueConvertible { + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return (self as! ReferenceType).databaseValue + } + + /// Returns a value initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? { + return ReferenceType.fromDatabaseValue(dbValue).flatMap { cast($0) } + } +} + +public extension DatabaseValueConvertible where Self: Decodable & ReferenceConvertible, Self.ReferenceType: DatabaseValueConvertible { + public static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Self? { + // Preserve custom database decoding + return ReferenceType.fromDatabaseValue(databaseValue).flatMap { cast($0) } + } +} + +public extension DatabaseValueConvertible where Self: Encodable & ReferenceConvertible, Self.ReferenceType: DatabaseValueConvertible { + public var databaseValue: DatabaseValue { + // Preserve custom database encoding + return (self as! ReferenceType).databaseValue + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/Support/Foundation/Date.swift b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/Date.swift new file mode 100644 index 0000000..f19b0d9 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/Date.swift @@ -0,0 +1,126 @@ +import Foundation + +/// NSDate is stored in the database using the format +/// "yyyy-MM-dd HH:mm:ss.SSS", in the UTC time zone. +extension NSDate : DatabaseValueConvertible { + /// Returns a database value that contains the date encoded as + /// "yyyy-MM-dd HH:mm:ss.SSS", in the UTC time zone. + public var databaseValue: DatabaseValue { + return (self as Date).databaseValue + } + + /// Returns a date initialized from dbValue, if possible. + /// + /// If database value contains a number, that number is interpreted as a + /// timeinterval since 00:00:00 UTC on 1 January 1970. + /// + /// If database value contains a string, that string is interpreted as a + /// [SQLite date](https://sqlite.org/lang_datefunc.html) in the UTC time + /// zone. Nil is returned if the date string does not contain at least the + /// year, month and day components. Other components (minutes, etc.) + /// are set to zero if missing. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? { + guard let date = Date.fromDatabaseValue(dbValue) else { + return nil + } + return cast(date) + } +} + +/// Date is stored in the database using the format +/// "yyyy-MM-dd HH:mm:ss.SSS", in the UTC time zone. +extension Date : DatabaseValueConvertible { + /// Returns a database value that contains the date encoded as + /// "yyyy-MM-dd HH:mm:ss.SSS", in the UTC time zone. + public var databaseValue: DatabaseValue { + return storageDateFormatter.string(from: self).databaseValue + } + + /// Returns a date initialized from dbValue, if possible. + /// + /// If database value contains a number, that number is interpreted as a + /// timeinterval since 00:00:00 UTC on 1 January 1970. + /// + /// If database value contains a string, that string is interpreted as a + /// [SQLite date](https://sqlite.org/lang_datefunc.html) in the UTC time + /// zone. Nil is returned if the date string does not contain at least the + /// year, month and day components. Other components (minutes, etc.) + /// are set to zero if missing. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Date? { + if let databaseDateComponents = DatabaseDateComponents.fromDatabaseValue(dbValue) { + return Date(databaseDateComponents: databaseDateComponents) + } + if let timestamp = Double.fromDatabaseValue(dbValue) { + return Date(timeIntervalSince1970: timestamp) + } + return nil + } + + private init?(databaseDateComponents: DatabaseDateComponents) { + guard databaseDateComponents.format.hasYMDComponents else { + // Refuse to turn hours without any date information into Date: + return nil + } + guard let date = UTCCalendar.date(from: databaseDateComponents.dateComponents) else { + return nil + } + self.init(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate) + } + + /// Creates a date from a [Julian Day](https://en.wikipedia.org/wiki/Julian_day). + public init?(julianDay: Double) { + // Conversion uses the same algorithm as SQLite: https://www.sqlite.org/src/artifact/8ec787fed4929d8c + // TODO: check for overflows one day, and return nil when computation can't complete. + let JD = Int64(julianDay * 86400000) + let Z = Int(((JD + 43200000)/86400000)) + var A = Int(((Double(Z) - 1867216.25)/36524.25)) + A = Z + 1 + A - (A/4) + let B = A + 1524 + let C = Int(((Double(B) - 122.1)/365.25)) + let D = (36525*(C&32767))/100 + let E = Int((Double(B-D)/30.6001)) + let X1 = Int((30.6001*Double(E))) + let day = B - D - X1 + let month = E<14 ? E-1 : E-13 + let year = month>2 ? C - 4716 : C - 4715 + var s = Int(((JD + 43200000) % 86400000)) + var second = Double(s)/1000.0 + s = Int(second) + second -= Double(s) + let hour = s/3600 + s -= hour*3600 + let minute = s/60 + second += Double(s - minute*60) + + var dateComponents = DateComponents() + dateComponents.year = year + dateComponents.month = month + dateComponents.day = day + dateComponents.hour = hour + dateComponents.minute = minute + dateComponents.second = Int(second) + dateComponents.nanosecond = Int((second - Double(Int(second))) * 1.0e9) + + guard let date = UTCCalendar.date(from: dateComponents) else { + return nil + } + self.init(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate) + } +} + +/// The DatabaseDate date formatter for stored dates. +private let storageDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + return formatter + }() + +// The NSCalendar for stored dates. +private let UTCCalendar: Calendar = { + var calendar = Calendar(identifier: .gregorian) + calendar.locale = Locale(identifier: "en_US_POSIX") + calendar.timeZone = TimeZone(secondsFromGMT: 0)! + return calendar + }() diff --git a/Pods/GRDB.swift/GRDB/Core/Support/Foundation/NSData.swift b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/NSData.swift new file mode 100644 index 0000000..28a7ae8 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/NSData.swift @@ -0,0 +1,19 @@ +import Foundation + +/// NSData is convertible to and from DatabaseValue. +extension NSData : DatabaseValueConvertible { + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return (self as Data).databaseValue + } + + /// Returns an NSData initialized from *dbValue*, if it contains + /// a Blob. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? { + guard let data = Data.fromDatabaseValue(dbValue) else { + return nil + } + return cast(data) + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/Support/Foundation/NSNull.swift b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/NSNull.swift new file mode 100644 index 0000000..e954bef --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/NSNull.swift @@ -0,0 +1,15 @@ +import Foundation + +/// NSNull adopts DatabaseValueConvertible +extension NSNull : DatabaseValueConvertible { + + /// Returns DatabaseValue.null. + public var databaseValue: DatabaseValue { + return .null + } + + /// Returns nil. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? { + return nil + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/Support/Foundation/NSNumber.swift b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/NSNumber.swift new file mode 100644 index 0000000..2a00c16 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/NSNumber.swift @@ -0,0 +1,67 @@ +import Foundation + +private let integerRoundingBehavior = NSDecimalNumberHandler(roundingMode: .plain, scale: 0, raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: false) + +/// NSNumber adopts DatabaseValueConvertible +extension NSNumber : DatabaseValueConvertible { + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + // Don't lose precision: store integers that fits in Int64 as Int64 + if let decimal = self as? NSDecimalNumber, + decimal == decimal.rounding(accordingToBehavior: integerRoundingBehavior), // integer + decimal.compare(NSDecimalNumber(value: Int64.max)) != .orderedDescending, // decimal <= Int64.max + decimal.compare(NSDecimalNumber(value: Int64.min)) != .orderedAscending // decimal >= Int64.min + { + return int64Value.databaseValue + } + + switch String(cString: objCType) { + case "c": + return Int64(int8Value).databaseValue + case "C": + return Int64(uint8Value).databaseValue + case "s": + return Int64(int16Value).databaseValue + case "S": + return Int64(uint16Value).databaseValue + case "i": + return Int64(int32Value).databaseValue + case "I": + return Int64(uint32Value).databaseValue + case "l": + return Int64(intValue).databaseValue + case "L": + let uint = uintValue + GRDBPrecondition(UInt64(uint) <= UInt64(Int64.max), "could not convert \(uint) to an Int64 that can be stored in the database") + return Int64(uint).databaseValue + case "q": + return Int64(int64Value).databaseValue + case "Q": + let uint64 = uint64Value + GRDBPrecondition(uint64 <= UInt64(Int64.max), "could not convert \(uint64) to an Int64 that can be stored in the database") + return Int64(uint64).databaseValue + case "f": + return Double(floatValue).databaseValue + case "d": + return doubleValue.databaseValue + case "B": + return boolValue.databaseValue + case let objCType: + // Assume a GRDB bug: there is no point throwing any error. + fatalError("DatabaseValueConvertible: Unsupported NSNumber type: \(objCType)") + } + } + + /// Returns an NSNumber initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? { + switch dbValue.storage { + case .int64(let int64): + return self.init(value: int64) + case .double(let double): + return self.init(value: double) + default: + return nil + } + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/Support/Foundation/NSString.swift b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/NSString.swift new file mode 100644 index 0000000..5777259 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/NSString.swift @@ -0,0 +1,18 @@ +import Foundation + +/// NSString adopts DatabaseValueConvertible +extension NSString : DatabaseValueConvertible { + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return (self as String).databaseValue + } + + /// Returns an NSString initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? { + guard let string = String.fromDatabaseValue(dbValue) else { + return nil + } + return self.init(string: string) + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/Support/Foundation/URL.swift b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/URL.swift new file mode 100644 index 0000000..516dcc1 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/URL.swift @@ -0,0 +1,22 @@ +import Foundation + +/// NSURL stores its absoluteString in the database. +extension NSURL : DatabaseValueConvertible { + + /// Returns a value that can be stored in the database. + /// (the URL's absoluteString). + public var databaseValue: DatabaseValue { + return absoluteString?.databaseValue ?? .null + } + + /// Returns an NSURL initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? { + guard let string = String.fromDatabaseValue(dbValue) else { + return nil + } + return cast(URL(string: string)) + } +} + +/// URL stores its absoluteString in the database. +extension URL : DatabaseValueConvertible { } diff --git a/Pods/GRDB.swift/GRDB/Core/Support/Foundation/UUID.swift b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/UUID.swift new file mode 100644 index 0000000..5d6d7cf --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Support/Foundation/UUID.swift @@ -0,0 +1,25 @@ +import Foundation + +/// NSUUID adopts DatabaseValueConvertible +extension NSUUID : DatabaseValueConvertible { + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + var uuidBytes = ContiguousArray(repeating: UInt8(0), count: 16) + return uuidBytes.withUnsafeMutableBufferPointer { buffer in + getBytes(buffer.baseAddress!) + return NSData(bytes: buffer.baseAddress, length: 16).databaseValue + } + } + + /// Returns an NSUUID initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? { + guard let data = NSData.fromDatabaseValue(dbValue), data.length == 16 else { + return nil + } + return self.init(uuidBytes: data.bytes.assumingMemoryBound(to: UInt8.self)) + } +} + +/// UUID adopts DatabaseValueConvertible +extension UUID : DatabaseValueConvertible { } diff --git a/Pods/GRDB.swift/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Decodable.swift b/Pods/GRDB.swift/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Decodable.swift new file mode 100644 index 0000000..a40e7ee --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Decodable.swift @@ -0,0 +1,91 @@ +private struct DatabaseValueDecodingContainer: SingleValueDecodingContainer { + let dbValue: DatabaseValue + let codingPath: [CodingKey] + + /// Decodes a null value. + /// + /// - returns: Whether the encountered value was null. + func decodeNil() -> Bool { + return dbValue.isNull + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the encountered encoded value cannot be converted to the requested type. + /// - throws: `DecodingError.valueNotFound` if the encountered encoded value is null. + func decode(_ type: Bool.Type) throws -> Bool { return dbValue.losslessConvert() } + func decode(_ type: Int.Type) throws -> Int { return dbValue.losslessConvert() } + func decode(_ type: Int8.Type) throws -> Int8 { return dbValue.losslessConvert() } + func decode(_ type: Int16.Type) throws -> Int16 { return dbValue.losslessConvert() } + func decode(_ type: Int32.Type) throws -> Int32 { return dbValue.losslessConvert() } + func decode(_ type: Int64.Type) throws -> Int64 { return dbValue.losslessConvert() } + func decode(_ type: UInt.Type) throws -> UInt { return dbValue.losslessConvert() } + func decode(_ type: UInt8.Type) throws -> UInt8 { return dbValue.losslessConvert() } + func decode(_ type: UInt16.Type) throws -> UInt16 { return dbValue.losslessConvert() } + func decode(_ type: UInt32.Type) throws -> UInt32 { return dbValue.losslessConvert() } + func decode(_ type: UInt64.Type) throws -> UInt64 { return dbValue.losslessConvert() } + func decode(_ type: Float.Type) throws -> Float { return dbValue.losslessConvert() } + func decode(_ type: Double.Type) throws -> Double { return dbValue.losslessConvert() } + func decode(_ type: String.Type) throws -> String { return dbValue.losslessConvert() } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the encountered encoded value cannot be converted to the requested type. + /// - throws: `DecodingError.valueNotFound` if the encountered encoded value is null. + func decode<T>(_ type: T.Type) throws -> T where T : Decodable { + if let type = T.self as? DatabaseValueConvertible.Type { + // Prefer DatabaseValueConvertible decoding over Decodable. + // This allows custom database decoding, such as decoding Date from + // String, for example. + return type.fromDatabaseValue(dbValue) as! T + } else { + return try T(from: DatabaseValueDecoder(dbValue: dbValue, codingPath: codingPath)) + } + } +} + +private struct DatabaseValueDecoder: Decoder { + let dbValue: DatabaseValue + + init(dbValue: DatabaseValue, codingPath: [CodingKey]) { + self.dbValue = dbValue + self.codingPath = codingPath + } + + // Decoder + let codingPath: [CodingKey] + var userInfo: [CodingUserInfoKey : Any] { return [:] } + + func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> { + throw DecodingError.typeMismatch( + type, + DecodingError.Context(codingPath: codingPath, debugDescription: "keyed decoding is not supported")) + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + throw DecodingError.typeMismatch( + UnkeyedDecodingContainer.self, + DecodingError.Context(codingPath: codingPath, debugDescription: "unkeyed decoding is not supported")) + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + return DatabaseValueDecodingContainer(dbValue: dbValue, codingPath: codingPath) + } +} + +public extension DatabaseValueConvertible where Self: Decodable { + public static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Self? { + return try? self.init(from: DatabaseValueDecoder(dbValue: databaseValue, codingPath: [])) + } +} + +public extension DatabaseValueConvertible where Self: Decodable & RawRepresentable, Self.RawValue: DatabaseValueConvertible { + public static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Self? { + // Preserve custom database decoding + return RawValue.fromDatabaseValue(databaseValue).flatMap { self.init(rawValue: $0) } + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Encodable.swift b/Pods/GRDB.swift/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Encodable.swift new file mode 100644 index 0000000..c7bf480 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Encodable.swift @@ -0,0 +1,106 @@ +private struct DatabaseValueEncodingContainer : SingleValueEncodingContainer { + let encode: (DatabaseValue) -> Void + + var codingPath: [CodingKey] { return [] } + + /// Encodes a null value. + /// + /// - throws: `EncodingError.invalidValue` if a null value is invalid in the current context for this format. + /// - precondition: May not be called after a previous `self.encode(_:)` call. + mutating func encodeNil() throws { encode(.null) } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in the current context for this format. + /// - precondition: May not be called after a previous `self.encode(_:)` call. + mutating func encode(_ value: Bool) throws { encode(value.databaseValue) } + mutating func encode(_ value: Int) throws { encode(value.databaseValue) } + mutating func encode(_ value: Int8) throws { encode(value.databaseValue) } + mutating func encode(_ value: Int16) throws { encode(value.databaseValue) } + mutating func encode(_ value: Int32) throws { encode(value.databaseValue) } + mutating func encode(_ value: Int64) throws { encode(value.databaseValue) } + mutating func encode(_ value: UInt) throws { encode(value.databaseValue) } + mutating func encode(_ value: UInt8) throws { encode(value.databaseValue) } + mutating func encode(_ value: UInt16) throws { encode(value.databaseValue) } + mutating func encode(_ value: UInt32) throws { encode(value.databaseValue) } + mutating func encode(_ value: UInt64) throws { encode(value.databaseValue) } + mutating func encode(_ value: Float) throws { encode(value.databaseValue) } + mutating func encode(_ value: Double) throws { encode(value.databaseValue) } + mutating func encode(_ value: String) throws { encode(value.databaseValue) } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in the current context for this format. + /// - precondition: May not be called after a previous `self.encode(_:)` call. + mutating func encode<T>(_ value: T) throws where T : Encodable { + if let dbValueConvertible = value as? DatabaseValueConvertible { + // Prefer DatabaseValueConvertible encoding over Decodable. + // This allows us to encode Date as String, for example. + encode(dbValueConvertible.databaseValue) + } else { + try value.encode(to: DatabaseValueEncoder(encode: encode)) + } + } +} + +private struct DatabaseValueEncoder : Encoder { + let encode: (DatabaseValue) -> Void + + /// The path of coding keys taken to get to this point in encoding. + /// A `nil` value indicates an unkeyed container. + var codingPath: [CodingKey] { return [] } + + /// Any contextual information set by the user for encoding. + var userInfo: [CodingUserInfoKey : Any] = [:] + + init(encode: @escaping (DatabaseValue) -> Void) { + self.encode = encode + } + + /// Returns an encoding container appropriate for holding multiple values keyed by the given key type. + /// + /// - parameter type: The key type to use for the container. + /// - returns: A new keyed encoding container. + /// - precondition: May not be called after a prior `self.unkeyedContainer()` call. + /// - precondition: May not be called after a value has been encoded through a previous `self.singleValueContainer()` call. + func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> { + fatalError("keyed encoding is not supported") + } + + /// Returns an encoding container appropriate for holding multiple unkeyed values. + /// + /// - returns: A new empty unkeyed container. + /// - precondition: May not be called after a prior `self.container(keyedBy:)` call. + /// - precondition: May not be called after a value has been encoded through a previous `self.singleValueContainer()` call. + func unkeyedContainer() -> UnkeyedEncodingContainer { + fatalError("unkeyed encoding is not supported") + } + + /// Returns an encoding container appropriate for holding a single primitive value. + /// + /// - returns: A new empty single value container. + /// - precondition: May not be called after a prior `self.container(keyedBy:)` call. + /// - precondition: May not be called after a prior `self.unkeyedContainer()` call. + /// - precondition: May not be called after a value has been encoded through a previous `self.singleValueContainer()` call. + func singleValueContainer() -> SingleValueEncodingContainer { + return DatabaseValueEncodingContainer(encode: encode) + } +} + +public extension DatabaseValueConvertible where Self: Encodable { + public var databaseValue: DatabaseValue { + var dbValue: DatabaseValue! = nil + let encoder = DatabaseValueEncoder(encode: { dbValue = $0 }) + try! self.encode(to: encoder) + return dbValue + } +} + +public extension DatabaseValueConvertible where Self: Encodable & RawRepresentable, Self.RawValue: DatabaseValueConvertible { + public var databaseValue: DatabaseValue { + // Preserve custom database encoding + return rawValue.databaseValue + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+RawRepresentable.swift b/Pods/GRDB.swift/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+RawRepresentable.swift new file mode 100644 index 0000000..0bb05cb --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+RawRepresentable.swift @@ -0,0 +1,24 @@ +/// DatabaseValueConvertible is free for RawRepresentable types whose raw value +/// is itself DatabaseValueConvertible. +/// +/// // If the RawValue adopts DatabaseValueConvertible... +/// enum Color : Int { +/// case red +/// case white +/// case rose +/// } +/// +/// // ... then the RawRepresentable type can freely adopt DatabaseValueConvertible: +/// extension Color : DatabaseValueConvertible { /* empty */ } +extension DatabaseValueConvertible where Self: RawRepresentable, Self.RawValue: DatabaseValueConvertible { + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return rawValue.databaseValue + } + + /// Returns a value initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? { + return RawValue.fromDatabaseValue(dbValue).flatMap { self.init(rawValue: $0) } + } +} diff --git a/Pods/GRDB.swift/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift b/Pods/GRDB.swift/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift new file mode 100644 index 0000000..cabe0a5 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift @@ -0,0 +1,695 @@ +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif + +// MARK: - Value Types + +/// Bool adopts DatabaseValueConvertible and StatementColumnConvertible. +extension Bool: DatabaseValueConvertible, StatementColumnConvertible { + + /// Returns a value initialized from a raw SQLite statement pointer. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. + public init(sqliteStatement: SQLiteStatement, index: Int32) { + self = sqlite3_column_int64(sqliteStatement, index) != 0 + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return (self ? 1 : 0).databaseValue + } + + /// Returns a Bool initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Bool? { + // IMPLEMENTATION NOTE + // + // https://www.sqlite.org/lang_expr.html#booleanexpr + // + // > # Boolean Expressions + // > + // > The SQL language features several contexts where an expression is + // > evaluated and the result converted to a boolean (true or false) + // > value. These contexts are: + // > + // > - the WHERE clause of a SELECT, UPDATE or DELETE statement, + // > - the ON or USING clause of a join in a SELECT statement, + // > - the HAVING clause of a SELECT statement, + // > - the WHEN clause of an SQL trigger, and + // > - the WHEN clause or clauses of some CASE expressions. + // > + // > To convert the results of an SQL expression to a boolean value, + // > SQLite first casts the result to a NUMERIC value in the same way as + // > a CAST expression. A numeric zero value (integer value 0 or real + // > value 0.0) is considered to be false. A NULL value is still NULL. + // > All other values are considered true. + // > + // > For example, the values NULL, 0.0, 0, 'english' and '0' are all + // > considered to be false. Values 1, 1.0, 0.1, -0.1 and '1english' are + // > considered to be true. + // + // OK so we have to support boolean for all storage classes? + // Actually we won't, because of the SQLite boolean interpretation of + // strings: + // + // The doc says that "english" should be false, and "1english" should + // be true. I guess "-1english" and "0.1english" should be true also. + // And... what about "0.0e10english"? + // + // Ideally, we'd ask SQLite to perform the conversion itself, and return + // its own boolean interpretation of the string. Unfortunately, it looks + // like it is not so easy... + // + // So we could take a short route, and assume all strings are false, + // since most strings are falsey for SQLite. + // + // Considering all strings falsey is unfortunately very + // counter-intuitive. This is not the correct way to tackle the boolean + // problem. + // + // Instead, let's use the fact that the BOOLEAN typename has Numeric + // affinity (https://www.sqlite.org/datatype3.html), and that the doc + // says: + // + // > SQLite does not have a separate Boolean storage class. Instead, + // > Boolean values are stored as integers 0 (false) and 1 (true). + // + // So we extract bools from Integer and Real only. Integer because it is + // the natural boolean storage class, and Real because Numeric affinity + // store big numbers as Real. + + switch dbValue.storage { + case .int64(let int64): + return (int64 != 0) + case .double(let double): + return (double != 0.0) + default: + return nil + } + } +} + +/// Int adopts DatabaseValueConvertible and StatementColumnConvertible. +extension Int: DatabaseValueConvertible, StatementColumnConvertible { + + /// Returns a value initialized from a raw SQLite statement pointer. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. + public init(sqliteStatement: SQLiteStatement, index: Int32) { + let int64 = sqlite3_column_int64(sqliteStatement, index) + if let v = Int(exactly: int64) { + self = v + } else { + fatalError("could not convert database value \(int64) to Int") + } + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return Int64(self).databaseValue + } + + /// Returns an Int initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Int? { + return Int64.fromDatabaseValue(dbValue).flatMap { Int(exactly: $0) } + } +} + +/// Int8 adopts DatabaseValueConvertible and StatementColumnConvertible. +extension Int8: DatabaseValueConvertible, StatementColumnConvertible { + + /// Returns a value initialized from a raw SQLite statement pointer. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. + public init(sqliteStatement: SQLiteStatement, index: Int32) { + let int64 = sqlite3_column_int64(sqliteStatement, index) + if let v = Int8(exactly: int64) { + self = v + } else { + fatalError("could not convert database value \(int64) to Int8") + } + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return Int64(self).databaseValue + } + + /// Returns an Int8 initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Int8? { + return Int64.fromDatabaseValue(dbValue).flatMap { Int8(exactly: $0) } + } +} + +/// Int16 adopts DatabaseValueConvertible and StatementColumnConvertible. +extension Int16: DatabaseValueConvertible, StatementColumnConvertible { + + /// Returns a value initialized from a raw SQLite statement pointer. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. + public init(sqliteStatement: SQLiteStatement, index: Int32) { + let int64 = sqlite3_column_int64(sqliteStatement, index) + if let v = Int16(exactly: int64) { + self = v + } else { + fatalError("could not convert database value \(int64) to Int16") + } + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return Int64(self).databaseValue + } + + /// Returns an Int16 initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Int16? { + return Int64.fromDatabaseValue(dbValue).flatMap { Int16(exactly: $0) } + } +} + +/// Int32 adopts DatabaseValueConvertible and StatementColumnConvertible. +extension Int32: DatabaseValueConvertible, StatementColumnConvertible { + + /// Returns a value initialized from a raw SQLite statement pointer. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. + public init(sqliteStatement: SQLiteStatement, index: Int32) { + let int64 = sqlite3_column_int64(sqliteStatement, index) + if let v = Int32(exactly: int64) { + self = v + } else { + fatalError("could not convert database value \(int64) to Int32") + } + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return Int64(self).databaseValue + } + + /// Returns an Int32 initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Int32? { + return Int64.fromDatabaseValue(dbValue).flatMap { Int32(exactly: $0) } + } +} + +/// Int64 adopts DatabaseValueConvertible and StatementColumnConvertible. +extension Int64: DatabaseValueConvertible, StatementColumnConvertible { + + /// Returns a value initialized from a raw SQLite statement pointer. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. + public init(sqliteStatement: SQLiteStatement, index: Int32) { + self = sqlite3_column_int64(sqliteStatement, index) + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return DatabaseValue(storage: .int64(self)) + } + + /// Returns an Int64 initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Int64? { + switch dbValue.storage { + case .int64(let int64): + return int64 + case .double(let double): + guard double >= Double(Int64.min) else { return nil } + guard double < Double(Int64.max) else { return nil } + return Int64(double) + default: + return nil + } + } +} + +/// UInt adopts DatabaseValueConvertible and StatementColumnConvertible. +extension UInt: DatabaseValueConvertible, StatementColumnConvertible { + + /// Returns a value initialized from a raw SQLite statement pointer. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. + public init(sqliteStatement: SQLiteStatement, index: Int32) { + let int64 = sqlite3_column_int64(sqliteStatement, index) + if let v = UInt(exactly: int64) { + self = v + } else { + fatalError("could not convert database value \(int64) to UInt") + } + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return Int64(self).databaseValue + } + + /// Returns an Int initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UInt? { + return Int64.fromDatabaseValue(dbValue).flatMap { UInt(exactly: $0) } + } +} + +/// UInt8 adopts DatabaseValueConvertible and StatementColumnConvertible. +extension UInt8: DatabaseValueConvertible, StatementColumnConvertible { + + /// Returns a value initialized from a raw SQLite statement pointer. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. + public init(sqliteStatement: SQLiteStatement, index: Int32) { + let int64 = sqlite3_column_int64(sqliteStatement, index) + if let v = UInt8(exactly: int64) { + self = v + } else { + fatalError("could not convert database value \(int64) to UInt8") + } + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return Int64(self).databaseValue + } + + /// Returns an UInt8 initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UInt8? { + return Int64.fromDatabaseValue(dbValue).flatMap { UInt8(exactly: $0) } + } +} + +/// UInt16 adopts DatabaseValueConvertible and StatementColumnConvertible. +extension UInt16: DatabaseValueConvertible, StatementColumnConvertible { + + /// Returns a value initialized from a raw SQLite statement pointer. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. + public init(sqliteStatement: SQLiteStatement, index: Int32) { + let int64 = sqlite3_column_int64(sqliteStatement, index) + if let v = UInt16(exactly: int64) { + self = v + } else { + fatalError("could not convert database value \(int64) to UInt16") + } + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return Int64(self).databaseValue + } + + /// Returns an UInt16 initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UInt16? { + return Int64.fromDatabaseValue(dbValue).flatMap { UInt16(exactly: $0) } + } +} + +/// UInt32 adopts DatabaseValueConvertible and StatementColumnConvertible. +extension UInt32: DatabaseValueConvertible, StatementColumnConvertible { + + /// Returns a value initialized from a raw SQLite statement pointer. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. + public init(sqliteStatement: SQLiteStatement, index: Int32) { + let int64 = sqlite3_column_int64(sqliteStatement, index) + if let v = UInt32(exactly: int64) { + self = v + } else { + fatalError("could not convert database value \(int64) to UInt32") + } + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return Int64(self).databaseValue + } + + /// Returns an UInt32 initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UInt32? { + return Int64.fromDatabaseValue(dbValue).flatMap { UInt32(exactly: $0) } + } +} + +/// UInt64 adopts DatabaseValueConvertible and StatementColumnConvertible. +extension UInt64: DatabaseValueConvertible, StatementColumnConvertible { + + /// Returns a value initialized from a raw SQLite statement pointer. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. + public init(sqliteStatement: SQLiteStatement, index: Int32) { + let int64 = sqlite3_column_int64(sqliteStatement, index) + if let v = UInt64(exactly: int64) { + self = v + } else { + fatalError("could not convert database value \(int64) to UInt64") + } + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return Int64(self).databaseValue + } + + /// Returns an UInt64 initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UInt64? { + return Int64.fromDatabaseValue(dbValue).flatMap { UInt64(exactly: $0) } + } +} + +/// Double adopts DatabaseValueConvertible and StatementColumnConvertible. +extension Double: DatabaseValueConvertible, StatementColumnConvertible { + + /// Returns a value initialized from a raw SQLite statement pointer. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. + public init(sqliteStatement: SQLiteStatement, index: Int32) { + self = sqlite3_column_double(sqliteStatement, index) + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return DatabaseValue(storage: .double(self)) + } + + /// Returns a Double initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Double? { + switch dbValue.storage { + case .int64(let int64): + return Double(int64) + case .double(let double): + return double + default: + return nil + } + } +} + +/// Float adopts DatabaseValueConvertible and StatementColumnConvertible. +extension Float: DatabaseValueConvertible, StatementColumnConvertible { + + /// Returns a value initialized from a raw SQLite statement pointer. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. + public init(sqliteStatement: SQLiteStatement, index: Int32) { + self = Float(sqlite3_column_double(sqliteStatement, index)) + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return Double(self).databaseValue + } + + /// Returns a Float initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Float? { + switch dbValue.storage { + case .int64(let int64): + return Float(int64) + case .double(let double): + return Float(double) + default: + return nil + } + } +} + +/// String adopts DatabaseValueConvertible and StatementColumnConvertible. +extension String: DatabaseValueConvertible, StatementColumnConvertible { + + /// Returns a value initialized from a raw SQLite statement pointer. + /// + /// - parameters: + /// - sqliteStatement: A pointer to an SQLite statement. + /// - index: The column index. + public init(sqliteStatement: SQLiteStatement, index: Int32) { + self = String(cString: sqlite3_column_text(sqliteStatement, Int32(index))!) + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return DatabaseValue(storage: .string(self)) + } + + /// Returns a String initialized from *dbValue*, if possible. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> String? { + switch dbValue.storage { + case .string(let string): + return string + default: + return nil + } + } +} + + +// MARK: - SQL Functions + +extension DatabaseFunction { + /// An SQL function that returns the Swift built-in capitalized + /// String property. + /// + /// The function returns NULL for non-strings values. + /// + /// This function is automatically added by GRDB to your database + /// connections. It is the function used by the query interface's + /// capitalized: + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn.capitalized) + /// let names = try String.fetchAll(dbQueue, request) // [String] + public static let capitalize = DatabaseFunction("swiftCapitalizedString", argumentCount: 1, pure: true) { dbValues in + guard let string = String.fromDatabaseValue(dbValues[0]) else { + return nil + } + return string.capitalized + } + + /// An SQL function that returns the Swift built-in lowercased + /// String property. + /// + /// The function returns NULL for non-strings values. + /// + /// This function is automatically added by GRDB to your database + /// connections. It is the function used by the query interface's + /// lowercased: + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn.lowercased()) + /// let names = try String.fetchAll(dbQueue, request) // [String] + public static let lowercase = DatabaseFunction("swiftLowercaseString", argumentCount: 1, pure: true) { dbValues in + guard let string = String.fromDatabaseValue(dbValues[0]) else { + return nil + } + return string.lowercased() + } + + /// An SQL function that returns the Swift built-in uppercased + /// String property. + /// + /// The function returns NULL for non-strings values. + /// + /// This function is automatically added by GRDB to your database + /// connections. It is the function used by the query interface's + /// uppercased: + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn.uppercased()) + /// let names = try String.fetchAll(dbQueue, request) // [String] + public static let uppercase = DatabaseFunction("swiftUppercaseString", argumentCount: 1, pure: true) { dbValues in + guard let string = String.fromDatabaseValue(dbValues[0]) else { + return nil + } + return string.uppercased() + } +} + +extension DatabaseFunction { + /// An SQL function that returns the Swift built-in + /// localizedCapitalized String property. + /// + /// The function returns NULL for non-strings values. + /// + /// This function is automatically added by GRDB to your database + /// connections. It is the function used by the query interface's + /// localizedCapitalized: + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn.localizedCapitalized) + /// let names = try String.fetchAll(dbQueue, request) // [String] + @available(iOS 9.0, OSX 10.11, watchOS 3.0, *) + public static let localizedCapitalize = DatabaseFunction("swiftLocalizedCapitalizedString", argumentCount: 1, pure: true) { dbValues in + guard let string = String.fromDatabaseValue(dbValues[0]) else { + return nil + } + return string.localizedCapitalized + } + + /// An SQL function that returns the Swift built-in + /// localizedLowercased String property. + /// + /// The function returns NULL for non-strings values. + /// + /// This function is automatically added by GRDB to your database + /// connections. It is the function used by the query interface's + /// localizedLowercased: + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn.localizedLowercased) + /// let names = try String.fetchAll(dbQueue, request) // [String] + @available(iOS 9.0, OSX 10.11, watchOS 3.0, *) + public static let localizedLowercase = DatabaseFunction("swiftLocalizedLowercaseString", argumentCount: 1, pure: true) { dbValues in + guard let string = String.fromDatabaseValue(dbValues[0]) else { + return nil + } + return string.localizedLowercase + } + + /// An SQL function that returns the Swift built-in + /// localizedUppercased String property. + /// + /// The function returns NULL for non-strings values. + /// + /// This function is automatically added by GRDB to your database + /// connections. It is the function used by the query interface's + /// localizedUppercased: + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn.localizedUppercased) + /// let names = try String.fetchAll(dbQueue, request) // [String] + @available(iOS 9.0, OSX 10.11, watchOS 3.0, *) + public static let localizedUppercase = DatabaseFunction("swiftLocalizedUppercaseString", argumentCount: 1, pure: true) { dbValues in + guard let string = String.fromDatabaseValue(dbValues[0]) else { + return nil + } + return string.localizedUppercase + } +} + + +// MARK: - SQLite Collations + +extension DatabaseCollation { + // Here we define a set of predefined collations. + // + // We should avoid renaming those collations, because database created with + // earlier versions of the library may have used those collations in the + // definition of tables. A renaming would prevent SQLite to find the + // collation. + // + // Yet we're not absolutely stuck: we could register support for obsolete + // collation names with sqlite3_collation_needed(). + // See https://www.sqlite.org/capi3ref.html#sqlite3_collation_needed + + /// A collation, or SQL string comparison function, that compares strings + /// according to the the Swift built-in == and <= operators. + /// + /// This collation is automatically added by GRDB to your database + /// connections. + /// + /// You can use it when creating database tables: + /// + /// let collationName = DatabaseCollation.caseInsensitiveCompare.name + /// dbQueue.execute( + /// "CREATE TABLE players (" + + /// "name TEXT COLLATE \(collationName)" + + /// ")" + /// ) + public static let unicodeCompare = DatabaseCollation("swiftCompare") { (lhs, rhs) in + return (lhs < rhs) ? .orderedAscending : ((lhs == rhs) ? .orderedSame : .orderedDescending) + } + + /// A collation, or SQL string comparison function, that compares strings + /// according to the the Swift built-in caseInsensitiveCompare(_:) method. + /// + /// This collation is automatically added by GRDB to your database + /// connections. + /// + /// You can use it when creating database tables: + /// + /// let collationName = DatabaseCollation.caseInsensitiveCompare.name + /// dbQueue.execute( + /// "CREATE TABLE players (" + + /// "name TEXT COLLATE \(collationName)" + + /// ")" + /// ) + public static let caseInsensitiveCompare = DatabaseCollation("swiftCaseInsensitiveCompare") { (lhs, rhs) in + return lhs.caseInsensitiveCompare(rhs) + } + + /// A collation, or SQL string comparison function, that compares strings + /// according to the the Swift built-in localizedCaseInsensitiveCompare(_:) method. + /// + /// This collation is automatically added by GRDB to your database + /// connections. + /// + /// You can use it when creating database tables: + /// + /// let collationName = DatabaseCollation.localizedCaseInsensitiveCompare.name + /// dbQueue.execute( + /// "CREATE TABLE players (" + + /// "name TEXT COLLATE \(collationName)" + + /// ")" + /// ) + public static let localizedCaseInsensitiveCompare = DatabaseCollation("swiftLocalizedCaseInsensitiveCompare") { (lhs, rhs) in + return lhs.localizedCaseInsensitiveCompare(rhs) + } + + /// A collation, or SQL string comparison function, that compares strings + /// according to the the Swift built-in localizedCompare(_:) method. + /// + /// This collation is automatically added by GRDB to your database + /// connections. + /// + /// You can use it when creating database tables: + /// + /// let collationName = DatabaseCollation.localizedCompare.name + /// dbQueue.execute( + /// "CREATE TABLE players (" + + /// "name TEXT COLLATE \(collationName)" + + /// ")" + /// ) + public static let localizedCompare = DatabaseCollation("swiftLocalizedCompare") { (lhs, rhs) in + return lhs.localizedCompare(rhs) + } + + /// A collation, or SQL string comparison function, that compares strings + /// according to the the Swift built-in localizedStandardCompare(_:) method. + /// + /// This collation is automatically added by GRDB to your database + /// connections. + /// + /// You can use it when creating database tables: + /// + /// let collationName = DatabaseCollation.localizedStandardCompare.name + /// dbQueue.execute( + /// "CREATE TABLE players (" + + /// "name TEXT COLLATE \(collationName)" + + /// ")" + /// ) + public static let localizedStandardCompare = DatabaseCollation("swiftLocalizedStandardCompare") { (lhs, rhs) in + return lhs.localizedStandardCompare(rhs) + } +} diff --git a/Pods/GRDB.swift/GRDB/FTS/FTS3.swift b/Pods/GRDB.swift/GRDB/FTS/FTS3.swift new file mode 100644 index 0000000..6e2e06f --- /dev/null +++ b/Pods/GRDB.swift/GRDB/FTS/FTS3.swift @@ -0,0 +1,81 @@ +/// FTS3 lets you define "fts3" virtual tables. +/// +/// // CREATE VIRTUAL TABLE documents USING fts3(content) +/// try db.create(virtualTable: "documents", using: FTS3()) { t in +/// t.column("content") +/// } +public struct FTS3 : VirtualTableModule { + /// Creates a FTS3 module suitable for the Database + /// `create(virtualTable:using:)` method. + /// + /// // CREATE VIRTUAL TABLE documents USING fts3(content) + /// try db.create(virtualTable: "documents", using: FTS3()) { t in + /// t.column("content") + /// } + public init() { + } + + // MARK: - VirtualTableModule Adoption + + /// The virtual table module name + public let moduleName = "fts3" + + /// Reserved; part of the VirtualTableModule protocol. + /// + /// See Database.create(virtualTable:using:) + public func makeTableDefinition() -> FTS3TableDefinition { + return FTS3TableDefinition() + } + + /// Reserved; part of the VirtualTableModule protocol. + /// + /// See Database.create(virtualTable:using:) + public func moduleArguments(for definition: FTS3TableDefinition, in db: Database) -> [String] { + var arguments = definition.columns + if let tokenizer = definition.tokenizer { + if tokenizer.arguments.isEmpty { + arguments.append("tokenize=\(tokenizer.name)") + } else { + arguments.append("tokenize=\(tokenizer.name) " + tokenizer.arguments.map { "\"\($0)\"" as String }.joined(separator: " ")) + } + } + return arguments + } + + /// Reserved; part of the VirtualTableModule protocol. + /// + /// See Database.create(virtualTable:using:) + public func database(_ db: Database, didCreate tableName: String, using definition: FTS3TableDefinition) { + } +} + +/// The FTS3TableDefinition class lets you define columns of a FTS3 virtual table. +/// +/// You don't create instances of this class. Instead, you use the Database +/// `create(virtualTable:using:)` method: +/// +/// try db.create(virtualTable: "documents", using: FTS3()) { t in // t is FTS3TableDefinition +/// t.column("content") +/// } +public final class FTS3TableDefinition { + fileprivate var columns: [String] = [] + + /// The virtual table tokenizer + /// + /// try db.create(virtualTable: "documents", using: FTS3()) { t in + /// t.tokenizer = .porter + /// } + /// See https://www.sqlite.org/fts3.html#creating_and_destroying_fts_tables + public var tokenizer: FTS3TokenizerDescriptor? + + /// Appends a table column. + /// + /// try db.create(virtualTable: "documents", using: FTS3()) { t in + /// t.column("content") + /// } + /// + /// - parameter name: the column name. + public func column(_ name: String) { + columns.append(name) + } +} diff --git a/Pods/GRDB.swift/GRDB/FTS/FTS3Pattern.swift b/Pods/GRDB.swift/GRDB/FTS/FTS3Pattern.swift new file mode 100644 index 0000000..2a2ef31 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/FTS/FTS3Pattern.swift @@ -0,0 +1,131 @@ +/// A full text pattern that can query FTS3 and FTS4 virtual tables. +public struct FTS3Pattern { + + /// The raw pattern string. Guaranteed to be a valid FTS3/4 pattern. + public let rawPattern: String + + /// Creates a pattern from a raw pattern string; throws DatabaseError on + /// invalid syntax. + /// + /// The pattern syntax is documented at https://www.sqlite.org/fts3.html#full_text_index_queries + /// + /// try FTS3Pattern(rawPattern: "and") // OK + /// try FTS3Pattern(rawPattern: "AND") // malformed MATCH expression: [AND] + public init(rawPattern: String) throws { + // Correctness above all: use SQLite to validate the pattern. + // + // Invalid patterns have SQLite return an error on the first + // call to sqlite3_step() on a statement that matches against + // that pattern. + do { + try DatabaseQueue().inDatabase { db in + try db.execute("CREATE VIRTUAL TABLE documents USING fts3()") + try db.execute("SELECT * FROM documents WHERE content MATCH ?", arguments: [rawPattern]) + } + } catch let error as DatabaseError { + // Remove private SQL & arguments from the thrown error + throw DatabaseError(resultCode: error.extendedResultCode, message: error.message, sql: nil, arguments: nil) + } + + // Pattern is valid + self.rawPattern = rawPattern + } + + #if GRDBCUSTOMSQLITE || GRDBCIPHER + /// Creates a pattern that matches any token found in the input string; + /// returns nil if no pattern could be built. + /// + /// FTS3Pattern(matchingAnyTokenIn: "") // nil + /// FTS3Pattern(matchingAnyTokenIn: "foo bar") // foo OR bar + /// + /// - parameter string: The string to turn into an FTS3 pattern + public init?(matchingAnyTokenIn string: String) { + let tokens = FTS3TokenizerDescriptor.simple.tokenize(string) + guard !tokens.isEmpty else { return nil } + try? self.init(rawPattern: tokens.joined(separator: " OR ")) + } + + /// Creates a pattern that matches all tokens found in the input string; + /// returns nil if no pattern could be built. + /// + /// FTS3Pattern(matchingAllTokensIn: "") // nil + /// FTS3Pattern(matchingAllTokensIn: "foo bar") // foo bar + /// + /// - parameter string: The string to turn into an FTS3 pattern + public init?(matchingAllTokensIn string: String) { + let tokens = FTS3TokenizerDescriptor.simple.tokenize(string) + guard !tokens.isEmpty else { return nil } + try? self.init(rawPattern: tokens.joined(separator: " ")) + } + + /// Creates a pattern that matches a contiguous string; returns nil if no + /// pattern could be built. + /// + /// FTS3Pattern(matchingPhrase: "") // nil + /// FTS3Pattern(matchingPhrase: "foo bar") // "foo bar" + /// + /// - parameter string: The string to turn into an FTS3 pattern + public init?(matchingPhrase string: String) { + let tokens = FTS3TokenizerDescriptor.simple.tokenize(string) + guard !tokens.isEmpty else { return nil } + try? self.init(rawPattern: "\"" + tokens.joined(separator: " ") + "\"") + } + #else + /// Creates a pattern that matches any token found in the input string; + /// returns nil if no pattern could be built. + /// + /// FTS3Pattern(matchingAnyTokenIn: "") // nil + /// FTS3Pattern(matchingAnyTokenIn: "foo bar") // foo OR bar + /// + /// - parameter string: The string to turn into an FTS3 pattern + @available(iOS 8.2, OSX 10.10, *) + public init?(matchingAnyTokenIn string: String) { + let tokens = FTS3TokenizerDescriptor.simple.tokenize(string) + guard !tokens.isEmpty else { return nil } + try? self.init(rawPattern: tokens.joined(separator: " OR ")) + } + + /// Creates a pattern that matches all tokens found in the input string; + /// returns nil if no pattern could be built. + /// + /// FTS3Pattern(matchingAllTokensIn: "") // nil + /// FTS3Pattern(matchingAllTokensIn: "foo bar") // foo bar + /// + /// - parameter string: The string to turn into an FTS3 pattern + @available(iOS 8.2, OSX 10.10, *) + public init?(matchingAllTokensIn string: String) { + let tokens = FTS3TokenizerDescriptor.simple.tokenize(string) + guard !tokens.isEmpty else { return nil } + try? self.init(rawPattern: tokens.joined(separator: " ")) + } + + /// Creates a pattern that matches a contiguous string; returns nil if no + /// pattern could be built. + /// + /// FTS3Pattern(matchingPhrase: "") // nil + /// FTS3Pattern(matchingPhrase: "foo bar") // "foo bar" + /// + /// - parameter string: The string to turn into an FTS3 pattern + @available(iOS 8.2, OSX 10.10, *) + public init?(matchingPhrase string: String) { + let tokens = FTS3TokenizerDescriptor.simple.tokenize(string) + guard !tokens.isEmpty else { return nil } + try? self.init(rawPattern: "\"" + tokens.joined(separator: " ") + "\"") + } + #endif +} + +extension FTS3Pattern : DatabaseValueConvertible { + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return rawPattern.databaseValue + } + + /// Returns an FTS3Pattern initialized from *dbValue*, if it contains + /// a suitable value. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> FTS3Pattern? { + return String + .fromDatabaseValue(dbValue) + .flatMap { try? FTS3Pattern(rawPattern: $0) } + } +} diff --git a/Pods/GRDB.swift/GRDB/FTS/FTS3TokenizerDescriptor.swift b/Pods/GRDB.swift/GRDB/FTS/FTS3TokenizerDescriptor.swift new file mode 100644 index 0000000..d4387dd --- /dev/null +++ b/Pods/GRDB.swift/GRDB/FTS/FTS3TokenizerDescriptor.swift @@ -0,0 +1,123 @@ +/// An FTS3 tokenizer, suitable for FTS3 and FTS4 table definitions: +/// +/// db.create(virtualTable: "books", using: FTS4()) { t in +/// t.tokenizer = .simple // FTS3TokenizerDescriptor +/// } +/// +/// See https://www.sqlite.org/fts3.html#tokenizer +public struct FTS3TokenizerDescriptor { + let name: String + let arguments: [String] + + init(_ name: String, arguments: [String] = []) { + self.name = name + self.arguments = arguments + } + + /// The "simple" tokenizer. + /// + /// db.create(virtualTable: "books", using: FTS4()) { t in + /// t.tokenizer = .simple + /// } + /// + /// See https://www.sqlite.org/fts3.html#tokenizer + public static let simple = FTS3TokenizerDescriptor("simple") + + /// The "porter" tokenizer. + /// + /// db.create(virtualTable: "books", using: FTS4()) { t in + /// t.tokenizer = .porter + /// } + /// + /// See https://www.sqlite.org/fts3.html#tokenizer + public static let porter = FTS3TokenizerDescriptor("porter") + + #if GRDBCUSTOMSQLITE || GRDBCIPHER + /// The "unicode61" tokenizer. + /// + /// db.create(virtualTable: "books", using: FTS4()) { t in + /// t.tokenizer = .unicode61() + /// } + /// + /// - parameters: + /// - removeDiacritics: If true (the default), then SQLite will strip + /// diacritics from latin characters. + /// - separators: Unless empty (the default), SQLite will consider these + /// characters as token separators. + /// - tokenCharacters: Unless empty (the default), SQLite will consider + /// these characters as token characters. + /// + /// See https://www.sqlite.org/fts3.html#tokenizer + public static func unicode61(removeDiacritics: Bool = true, separators: Set<Character> = [], tokenCharacters: Set<Character> = []) -> FTS3TokenizerDescriptor { + return _unicode61(removeDiacritics: removeDiacritics, separators: separators, tokenCharacters: tokenCharacters) + } + #else + /// The "unicode61" tokenizer. + /// + /// db.create(virtualTable: "books", using: FTS4()) { t in + /// t.tokenizer = .unicode61() + /// } + /// + /// - parameters: + /// - removeDiacritics: If true (the default), then SQLite will strip + /// diacritics from latin characters. + /// - separators: Unless empty (the default), SQLite will consider these + /// characters as token separators. + /// - tokenCharacters: Unless empty (the default), SQLite will consider + /// these characters as token characters. + /// + /// See https://www.sqlite.org/fts3.html#tokenizer + @available(iOS 8.2, OSX 10.10, *) + public static func unicode61(removeDiacritics: Bool = true, separators: Set<Character> = [], tokenCharacters: Set<Character> = []) -> FTS3TokenizerDescriptor { + // query_only pragma was added in SQLite 3.8.0 http://www.sqlite.org/changes.html#version_3_8_0 + // It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS) + return _unicode61(removeDiacritics: removeDiacritics, separators: separators, tokenCharacters: tokenCharacters) + } + #endif + + private static func _unicode61(removeDiacritics: Bool = true, separators: Set<Character> = [], tokenCharacters: Set<Character> = []) -> FTS3TokenizerDescriptor { + var arguments: [String] = [] + if !removeDiacritics { + arguments.append("remove_diacritics=0") + } + if !separators.isEmpty { + // TODO: test "=" and "\"", "(" and ")" as separators, with both FTS3Pattern(matchingAnyTokenIn:tokenizer:) and Database.create(virtualTable:using:) + arguments.append("separators=" + separators.sorted().map { String($0) }.joined(separator: "")) + } + if !tokenCharacters.isEmpty { + // TODO: test "=" and "\"", "(" and ")" as tokenCharacters, with both FTS3Pattern(matchingAnyTokenIn:tokenizer:) and Database.create(virtualTable:using:) + arguments.append("tokenchars=" + tokenCharacters.sorted().map { String($0) }.joined(separator: "")) + } + return FTS3TokenizerDescriptor("unicode61", arguments: arguments) + } + + #if GRDBCUSTOMSQLITE || GRDBCIPHER + func tokenize(_ string: String) -> [String] { + return _tokenize(string) + } + #else + @available(iOS 8.2, OSX 10.10, *) + func tokenize(_ string: String) -> [String] { + return _tokenize(string) + } + #endif + + /// Returns an array of tokens found in the string argument. + /// + /// FTS3TokenizerDescriptor.simple.tokenize("foo bar") // ["foo", "bar"] + private func _tokenize(_ string: String) -> [String] { + // fts3tokenize was introduced in SQLite 3.7.17 https://www.sqlite.org/changes.html#version_3_7_17 + // It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS) + return DatabaseQueue().inDatabase { db in + var tokenizerChunks: [String] = [] + tokenizerChunks.append(name) + for option in arguments { + tokenizerChunks.append("\"\(option)\"") + } + let tokenizerSQL = tokenizerChunks.joined(separator: ", ") + // Assume fts3tokenize virtual table in an in-memory database always succeeds + try! db.execute("CREATE VIRTUAL TABLE tokens USING fts3tokenize(\(tokenizerSQL))") + return try! String.fetchAll(db, "SELECT token FROM tokens WHERE input = ? ORDER BY position", arguments: [string]) + } + } +} diff --git a/Pods/GRDB.swift/GRDB/FTS/FTS4.swift b/Pods/GRDB.swift/GRDB/FTS/FTS4.swift new file mode 100644 index 0000000..21a376a --- /dev/null +++ b/Pods/GRDB.swift/GRDB/FTS/FTS4.swift @@ -0,0 +1,315 @@ +/// FTS4 lets you define "fts4" virtual tables. +/// +/// // CREATE VIRTUAL TABLE documents USING fts4(content) +/// try db.create(virtualTable: "documents", using: FTS4()) { t in +/// t.column("content") +/// } +/// +/// See https://www.sqlite.org/fts3.html +public struct FTS4 : VirtualTableModule { + + /// Creates a FTS4 module suitable for the Database + /// `create(virtualTable:using:)` method. + /// + /// // CREATE VIRTUAL TABLE documents USING fts4(content) + /// try db.create(virtualTable: "documents", using: FTS4()) { t in + /// t.column("content") + /// } + /// + /// See https://www.sqlite.org/fts3.html + public init() { + } + + // MARK: - VirtualTableModule Adoption + + /// The virtual table module name + public let moduleName = "fts4" + + /// Reserved; part of the VirtualTableModule protocol. + /// + /// See Database.create(virtualTable:using:) + public func makeTableDefinition() -> FTS4TableDefinition { + return FTS4TableDefinition() + } + + /// Reserved; part of the VirtualTableModule protocol. + /// + /// See Database.create(virtualTable:using:) + public func moduleArguments(for definition: FTS4TableDefinition, in db: Database) -> [String] { + var arguments: [String] = [] + + for column in definition.columns { + if column.isLanguageId { + arguments.append("languageid=\"\(column.name)\"") + } else { + arguments.append(column.name) + if !column.isIndexed { + arguments.append("notindexed=\(column.name)") + } + } + } + + if let tokenizer = definition.tokenizer { + if tokenizer.arguments.isEmpty { + arguments.append("tokenize=\(tokenizer.name)") + } else { + arguments.append("tokenize=\(tokenizer.name) " + tokenizer.arguments.map { "\"\($0)\"" as String }.joined(separator: " ")) + } + } + + switch definition.contentMode { + case .raw(let content): + if let content = content { + arguments.append("content=\"\(content)\"") + } + case .synchronized(let contentTable): + arguments.append("content=\"\(contentTable)\"") + } + + if let compress = definition.compress { + arguments.append("compress=\"\(compress)\"") + } + + if let uncompress = definition.uncompress { + arguments.append("uncompress=\"\(uncompress)\"") + } + + if let matchinfo = definition.matchinfo { + arguments.append("matchinfo=\"\(matchinfo)\"") + } + + if let prefixes = definition.prefixes { + arguments.append("prefix=\"\(prefixes.sorted().map { "\($0)" }.joined(separator: ","))\"") + } + + return arguments + } + + /// Reserved; part of the VirtualTableModule protocol. + /// + /// See Database.create(virtualTable:using:) + public func database(_ db: Database, didCreate tableName: String, using definition: FTS4TableDefinition) throws { + switch definition.contentMode { + case .raw: + break + case .synchronized(let contentTable): + // https://www.sqlite.org/fts3.html#_external_content_fts4_tables_ + + let rowIDColumn = try db.primaryKey(contentTable).rowIDColumn ?? Column.rowID.name + let ftsTable = tableName.quotedDatabaseIdentifier + let content = contentTable.quotedDatabaseIdentifier + let indexedColumns = definition.columns.map { $0.name } + + let ftsColumns = (["docid"] + indexedColumns) + .map { $0.quotedDatabaseIdentifier } + .joined(separator: ", ") + + let newContentColumns = ([rowIDColumn] + indexedColumns) + .map { "new.\($0.quotedDatabaseIdentifier)" } + .joined(separator: ", ") + + let oldRowID = "old.\(rowIDColumn.quotedDatabaseIdentifier)" + + try db.execute(""" + CREATE TRIGGER \("__\(contentTable)_bu".quotedDatabaseIdentifier) BEFORE UPDATE ON \(content) BEGIN + DELETE FROM \(ftsTable) WHERE docid=\(oldRowID); + END; + CREATE TRIGGER \("__\(contentTable)_bd".quotedDatabaseIdentifier) BEFORE DELETE ON \(content) BEGIN + DELETE FROM \(ftsTable) WHERE docid=\(oldRowID); + END; + CREATE TRIGGER \("__\(contentTable)_au".quotedDatabaseIdentifier) AFTER UPDATE ON \(content) BEGIN + INSERT INTO \(ftsTable)(\(ftsColumns)) VALUES(\(newContentColumns)); + END; + CREATE TRIGGER \("__\(contentTable)_ai".quotedDatabaseIdentifier) AFTER INSERT ON \(content) BEGIN + INSERT INTO \(ftsTable)(\(ftsColumns)) VALUES(\(newContentColumns)); + END; + """) + + // https://www.sqlite.org/fts3.html#*fts4rebuidcmd + + try db.execute("INSERT INTO \(ftsTable)(\(ftsTable)) VALUES('rebuild')") + } + } +} + +/// The FTS4TableDefinition class lets you define columns of a FTS4 virtual table. +/// +/// You don't create instances of this class. Instead, you use the Database +/// `create(virtualTable:using:)` method: +/// +/// try db.create(virtualTable: "documents", using: FTS4()) { t in // t is FTS4TableDefinition +/// t.column("content") +/// } +/// +/// See https://www.sqlite.org/fts3.html +public final class FTS4TableDefinition { + enum ContentMode { + case raw(content: String?) + case synchronized(contentTable: String) + } + + fileprivate var columns: [FTS4ColumnDefinition] = [] + fileprivate var contentMode: ContentMode = .raw(content: nil) + + /// The virtual table tokenizer + /// + /// try db.create(virtualTable: "documents", using: FTS4()) { t in + /// t.tokenizer = .porter + /// } + /// + /// See https://www.sqlite.org/fts3.html#creating_and_destroying_fts_tables + public var tokenizer: FTS3TokenizerDescriptor? + + /// The FTS4 `content` option + /// + /// When you want the full-text table to be synchronized with the + /// content of an external table, prefer the `synchronize(withTable:)` + /// method. + /// + /// Setting this property invalidates any synchronization previously + /// established with the `synchronize(withTable:)` method. + /// + /// See https://www.sqlite.org/fts3.html#the_content_option_ + public var content: String? { + get { + switch contentMode { + case .raw(let content): + return content + case .synchronized(let contentTable): + return contentTable + } + } + set { + contentMode = .raw(content: newValue) + } + } + + /// The FTS4 `compress` option + /// + /// See https://www.sqlite.org/fts3.html#the_compress_and_uncompress_options + public var compress: String? + + /// The FTS4 `uncompress` option + /// + /// See https://www.sqlite.org/fts3.html#the_compress_and_uncompress_options + public var uncompress: String? + + /// The FTS4 `matchinfo` option + /// + /// See https://www.sqlite.org/fts3.html#the_matchinfo_option + public var matchinfo: String? + + /// Support for the FTS5 `prefix` option + /// + /// // CREATE VIRTUAL TABLE documents USING FTS4(content, prefix='2 4'); + /// db.create(virtualTable: "documents", using:FTS4()) { t in + /// t.prefixes = [2, 4] + /// t.column("content") + /// } + /// + /// See https://www.sqlite.org/fts3.html#the_prefix_option + public var prefixes: Set<Int>? + + /// Appends a table column. + /// + /// try db.create(virtualTable: "documents", using: FTS4()) { t in + /// t.column("content") + /// } + /// + /// - parameter name: the column name. + @discardableResult + public func column(_ name: String) -> FTS4ColumnDefinition { + let column = FTS4ColumnDefinition(name: name) + columns.append(column) + return column + } + + /// Synchronizes the full-text table with the content of an external + /// table. + /// + /// The full-text table is initially populated with the existing + /// content in the external table. SQL triggers make sure that the + /// full-text table is kept up to date with the external table. + /// + /// See https://sqlite.org/fts5.html#external_content_tables + public func synchronize(withTable tableName: String) { + contentMode = .synchronized(contentTable: tableName) + } +} + +/// The FTS4ColumnDefinition class lets you refine a column of an FTS4 +/// virtual table. +/// +/// You get instances of this class when you create an FTS4 table: +/// +/// try db.create(virtualTable: "documents", using: FTS4()) { t in +/// t.column("content") // FTS4ColumnDefinition +/// } +/// +/// See https://www.sqlite.org/fts3.html +public final class FTS4ColumnDefinition { + fileprivate let name: String + fileprivate var isIndexed: Bool + fileprivate var isLanguageId: Bool + + init(name: String) { + self.name = name + self.isIndexed = true + self.isLanguageId = false + } + + #if GRDBCUSTOMSQLITE || GRDBCIPHER + /// Excludes the column from the full-text index. + /// + /// try db.create(virtualTable: "documents", using: FTS4()) { t in + /// t.column("a") + /// t.column("b").notIndexed() + /// } + /// + /// See https://www.sqlite.org/fts3.html#the_notindexed_option + /// + /// - returns: Self so that you can further refine the column definition. + @discardableResult + public func notIndexed() -> Self { + // notindexed FTS4 option was added in SQLite 3.8.0 http://www.sqlite.org/changes.html#version_3_8_0 + // It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS) + self.isIndexed = false + return self + } + #else + /// Excludes the column from the full-text index. + /// + /// try db.create(virtualTable: "documents", using: FTS4()) { t in + /// t.column("a") + /// t.column("b").notIndexed() + /// } + /// + /// See https://www.sqlite.org/fts3.html#the_notindexed_option + /// + /// - returns: Self so that you can further refine the column definition. + @available(iOS 8.2, OSX 10.10, *) + @discardableResult + public func notIndexed() -> Self { + // notindexed FTS4 option was added in SQLite 3.8.0 http://www.sqlite.org/changes.html#version_3_8_0 + // It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS) + self.isIndexed = false + return self + } + #endif + + /// Uses the column as the Int32 language id hidden column. + /// + /// try db.create(virtualTable: "documents", using: FTS4()) { t in + /// t.column("a") + /// t.column("lid").asLanguageId() + /// } + /// + /// See https://www.sqlite.org/fts3.html#the_languageid_option + /// + /// - returns: Self so that you can further refine the column definition. + @discardableResult + public func asLanguageId() -> Self { + self.isLanguageId = true + return self + } +} diff --git a/Pods/GRDB.swift/GRDB/FTS/FTS5.swift b/Pods/GRDB.swift/GRDB/FTS/FTS5.swift new file mode 100644 index 0000000..cb52263 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/FTS/FTS5.swift @@ -0,0 +1,322 @@ +#if SQLITE_ENABLE_FTS5 + /// FTS5 lets you define "fts5" virtual tables. + /// + /// // CREATE VIRTUAL TABLE documents USING fts5(content) + /// try db.create(virtualTable: "documents", using: FTS5()) { t in + /// t.column("content") + /// } + /// + /// See https://www.sqlite.org/fts5.html + public struct FTS5 : VirtualTableModule { + + /// Creates a FTS5 module suitable for the Database + /// `create(virtualTable:using:)` method. + /// + /// // CREATE VIRTUAL TABLE documents USING fts5(content) + /// try db.create(virtualTable: "documents", using: FTS5()) { t in + /// t.column("content") + /// } + /// + /// See https://www.sqlite.org/fts5.html + public init() { + } + + // MARK: - VirtualTableModule Adoption + + /// The virtual table module name + public let moduleName = "fts5" + + /// Don't use this method. + public func makeTableDefinition() -> FTS5TableDefinition { + return FTS5TableDefinition() + } + + /// Don't use this method. + public func moduleArguments(for definition: FTS5TableDefinition, in db: Database) throws -> [String] { + var arguments: [String] = [] + + if definition.columns.isEmpty { + // Programmer error + fatalError("FTS5 virtual table requires at least one column.") + } + + for column in definition.columns { + if column.isIndexed { + arguments.append("\(column.name)") + } else { + arguments.append("\(column.name) UNINDEXED") + } + } + + if let tokenizer = definition.tokenizer { + arguments.append("tokenize=\(tokenizer.components.joined(separator: " ").sqlExpression.sql)") + } + + switch definition.contentMode { + case .raw(let content, let contentRowID): + if let content = content { + arguments.append("content=\(content.sqlExpression.sql)") + } + if let contentRowID = contentRowID { + arguments.append("content_rowid=\(contentRowID.sqlExpression.sql)") + } + case .synchronized(let contentTable): + arguments.append("content=\(contentTable.sqlExpression.sql)") + if let rowIDColumn = try db.primaryKey(contentTable).rowIDColumn { + arguments.append("content_rowid=\(rowIDColumn.sqlExpression.sql)") + } + } + + + if let prefixes = definition.prefixes { + arguments.append("prefix=\(prefixes.map { "\($0)" }.joined(separator: " ").sqlExpression.sql)") + } + + if let columnSize = definition.columnSize { + arguments.append("columnSize=\(columnSize)") + } + + if let detail = definition.detail { + arguments.append("detail=\(detail)") + } + + return arguments + } + + /// Reserved; part of the VirtualTableModule protocol. + /// + /// See Database.create(virtualTable:using:) + public func database(_ db: Database, didCreate tableName: String, using definition: FTS5TableDefinition) throws { + switch definition.contentMode { + case .raw: + break + case .synchronized(let contentTable): + // https://sqlite.org/fts5.html#external_content_tables + + let rowIDColumn = try db.primaryKey(contentTable).rowIDColumn ?? Column.rowID.name + let ftsTable = tableName.quotedDatabaseIdentifier + let content = contentTable.quotedDatabaseIdentifier + let indexedColumns = definition.columns.map { $0.name } + + let ftsColumns = (["rowid"] + indexedColumns) + .map { $0.quotedDatabaseIdentifier } + .joined(separator: ", ") + + let newContentColumns = ([rowIDColumn] + indexedColumns) + .map { "new.\($0.quotedDatabaseIdentifier)" } + .joined(separator: ", ") + + let oldContentColumns = ([rowIDColumn] + indexedColumns) + .map { "old.\($0.quotedDatabaseIdentifier)" } + .joined(separator: ", ") + + try db.execute(""" + CREATE TRIGGER \("__\(contentTable)_ai".quotedDatabaseIdentifier) AFTER INSERT ON \(content) BEGIN + INSERT INTO \(ftsTable)(\(ftsColumns)) VALUES (\(newContentColumns)); + END; + CREATE TRIGGER \("__\(contentTable)_ad".quotedDatabaseIdentifier) AFTER DELETE ON \(content) BEGIN + INSERT INTO \(ftsTable)(\(ftsTable), \(ftsColumns)) VALUES('delete', \(oldContentColumns)); + END; + CREATE TRIGGER \("__\(contentTable)_au".quotedDatabaseIdentifier) AFTER UPDATE ON \(content) BEGIN + INSERT INTO \(ftsTable)(\(ftsTable), \(ftsColumns)) VALUES('delete', \(oldContentColumns)); + INSERT INTO \(ftsTable)(\(ftsColumns)) VALUES (\(newContentColumns)); + END; + """) + + // https://sqlite.org/fts5.html#the_rebuild_command + + try db.execute("INSERT INTO \(ftsTable)(\(ftsTable)) VALUES('rebuild')") + } + } + + static func api(_ db: Database) -> UnsafePointer<fts5_api> { + let sqliteConnection = db.sqliteConnection + var statement: SQLiteStatement? = nil + var api: UnsafePointer<fts5_api>? = nil + let type: StaticString = "fts5_api_ptr" + + let code = sqlite3_prepare_v3(db.sqliteConnection, "SELECT fts5(?)", -1, 0, &statement, nil) + guard code == SQLITE_OK else { + fatalError("FTS5 is not available") + } + defer { sqlite3_finalize(statement) } + type.utf8Start.withMemoryRebound(to: Int8.self, capacity: type.utf8CodeUnitCount) { typePointer in + _ = sqlite3_bind_pointer(statement, 1, &api, typePointer, nil) + } + sqlite3_step(statement) + guard let result = api else { + fatalError("FTS5 is not available") + } + return result + } + } + + /// The FTS5TableDefinition class lets you define columns of a FTS5 virtual table. + /// + /// You don't create instances of this class. Instead, you use the Database + /// `create(virtualTable:using:)` method: + /// + /// try db.create(virtualTable: "documents", using: FTS5()) { t in // t is FTS5TableDefinition + /// t.column("content") + /// } + /// + /// See https://www.sqlite.org/fts5.html + public final class FTS5TableDefinition { + enum ContentMode { + case raw(content: String?, contentRowID: String?) + case synchronized(contentTable: String) + } + + fileprivate var columns: [FTS5ColumnDefinition] = [] + fileprivate var contentMode: ContentMode = .raw(content: nil, contentRowID: nil) + + /// The virtual table tokenizer + /// + /// try db.create(virtualTable: "documents", using: FTS5()) { t in + /// t.tokenizer = .porter() + /// } + /// + /// See https://www.sqlite.org/fts5.html#fts5_table_creation_and_initialization + public var tokenizer: FTS5TokenizerDescriptor? + + /// The FTS5 `content` option + /// + /// When you want the full-text table to be synchronized with the + /// content of an external table, prefer the `synchronize(withTable:)` + /// method. + /// + /// Setting this property invalidates any synchronization previously + /// established with the `synchronize(withTable:)` method. + /// + /// See https://www.sqlite.org/fts5.html#external_content_and_contentless_tables + public var content: String? { + get { + switch contentMode { + case .raw(let content, _): + return content + case .synchronized(let contentTable): + return contentTable + } + } + set { + switch contentMode { + case .raw(_, let contentRowID): + contentMode = .raw(content: newValue, contentRowID: contentRowID) + case .synchronized: + contentMode = .raw(content: newValue, contentRowID: nil) + } + } + } + + /// The FTS5 `content_rowid` option + /// + /// When you want the full-text table to be synchronized with the + /// content of an external table, prefer the `synchronize(withTable:)` + /// method. + /// + /// Setting this property invalidates any synchronization previously + /// established with the `synchronize(withTable:)` method. + /// + /// See https://sqlite.org/fts5.html#external_content_tables + public var contentRowID: String? { + get { + switch contentMode { + case .raw(_, let contentRowID): + return contentRowID + case .synchronized: + return nil + } + } + set { + switch contentMode { + case .raw(let content, _): + contentMode = .raw(content: content, contentRowID: newValue) + case .synchronized: + contentMode = .raw(content: nil, contentRowID: newValue) + } + } + } + + /// Support for the FTS5 `prefix` option + /// + /// See https://www.sqlite.org/fts5.html#prefix_indexes + public var prefixes: Set<Int>? + + /// Support for the FTS5 `columnsize` option + /// + /// https://www.sqlite.org/fts5.html#the_columnsize_option + public var columnSize: Int? + + /// Support for the FTS5 `detail` option + /// + /// https://www.sqlite.org/fts5.html#the_detail_option + public var detail: String? + + /// Appends a table column. + /// + /// try db.create(virtualTable: "documents", using: FTS5()) { t in + /// t.column("content") + /// } + /// + /// - parameter name: the column name. + @discardableResult + public func column(_ name: String) -> FTS5ColumnDefinition { + let column = FTS5ColumnDefinition(name: name) + columns.append(column) + return column + } + + /// Synchronizes the full-text table with the content of an external + /// table. + /// + /// The full-text table is initially populated with the existing + /// content in the external table. SQL triggers make sure that the + /// full-text table is kept up to date with the external table. + /// + /// See https://sqlite.org/fts5.html#external_content_tables + public func synchronize(withTable tableName: String) { + contentMode = .synchronized(contentTable: tableName) + } + } + + /// The FTS5ColumnDefinition class lets you refine a column of an FTS5 + /// virtual table. + /// + /// You get instances of this class when you create an FTS5 table: + /// + /// try db.create(virtualTable: "documents", using: FTS5()) { t in + /// t.column("content") // FTS5ColumnDefinition + /// } + /// + /// See https://www.sqlite.org/fts5.html + public final class FTS5ColumnDefinition { + fileprivate let name: String + fileprivate var isIndexed: Bool + + init(name: String) { + self.name = name + self.isIndexed = true + } + + /// Excludes the column from the full-text index. + /// + /// try db.create(virtualTable: "documents", using: FTS5()) { t in + /// t.column("a") + /// t.column("b").notIndexed() + /// } + /// + /// See https://www.sqlite.org/fts5.html#the_unindexed_column_option + /// + /// - returns: Self so that you can further refine the column definition. + @discardableResult + public func notIndexed() -> Self { + self.isIndexed = false + return self + } + } + + extension Column { + /// The FTS5 rank column + public static let rank = Column("rank") + } +#endif diff --git a/Pods/GRDB.swift/GRDB/FTS/FTS5CustomTokenizer.swift b/Pods/GRDB.swift/GRDB/FTS/FTS5CustomTokenizer.swift new file mode 100644 index 0000000..357d9bc --- /dev/null +++ b/Pods/GRDB.swift/GRDB/FTS/FTS5CustomTokenizer.swift @@ -0,0 +1,168 @@ +#if SQLITE_ENABLE_FTS5 + /// The protocol for custom FTS5 tokenizers. + public protocol FTS5CustomTokenizer : FTS5Tokenizer { + /// The name of the tokenizer; should uniquely identify your custom + /// tokenizer. + static var name: String { get } + + /// Creates a custom tokenizer. + /// + /// The arguments parameter is an array of String built from the CREATE + /// VIRTUAL TABLE statement. In the example below, the arguments will + /// be `["arg1", "arg2"]`. + /// + /// CREATE VIRTUAL TABLE documents USING fts5( + /// tokenize='custom arg1 arg2' + /// ) + /// + /// - parameter db: A Database connection + /// - parameter arguments: An array of string arguments + init(db: Database, arguments: [String]) throws + } + + extension FTS5CustomTokenizer { + + /// Creates an FTS5 tokenizer descriptor. + /// + /// class MyTokenizer : FTS5CustomTokenizer { ... } + /// + /// db.create(virtualTable: "books", using: FTS5()) { t in + /// let tokenizer = MyTokenizer.tokenizerDescriptor(arguments: ["unicode61", "remove_diacritics", "0"]) + /// t.tokenizer = tokenizer + /// } + public static func tokenizerDescriptor(arguments: [String] = []) -> FTS5TokenizerDescriptor { + return FTS5TokenizerDescriptor(components: [name] + arguments) + } + + } + + extension Database { + + // MARK: - Custom FTS5 Tokenizers + + private class FTS5TokenizerConstructor { + let db: Database + let constructor: (Database, [String], UnsafeMutablePointer<OpaquePointer?>?) -> Int32 + + init(db: Database, constructor: @escaping (Database, [String], UnsafeMutablePointer<OpaquePointer?>?) -> Int32) { + self.db = db + self.constructor = constructor + } + } + + /// Add a custom FTS5 tokenizer. + /// + /// class MyTokenizer : FTS5CustomTokenizer { ... } + /// db.add(tokenizer: MyTokenizer.self) + public func add<Tokenizer: FTS5CustomTokenizer>(tokenizer: Tokenizer.Type) { + let api = FTS5.api(self) + + // Swift won't let the @convention(c) xCreate() function below create + // an instance of the generic Tokenizer type. + // + // We thus hide the generic Tokenizer type inside a neutral type: + // FTS5TokenizerConstructor + let constructor = FTS5TokenizerConstructor( + db: self, + constructor: { (db, arguments, tokenizerHandle) in + guard let tokenizerHandle = tokenizerHandle else { + return SQLITE_ERROR + } + do { + let tokenizer = try Tokenizer(db: db, arguments: arguments) + + // Tokenizer must remain alive until xDeleteTokenizer() + // is called, as the xDelete member of xTokenizer + let tokenizerPointer = OpaquePointer(Unmanaged.passRetained(tokenizer).toOpaque()) + + tokenizerHandle.pointee = tokenizerPointer + return SQLITE_OK + } catch let error as DatabaseError { + return error.extendedResultCode.rawValue + } catch { + return SQLITE_ERROR + } + }) + + // Constructor must remain alive until deleteConstructor() is + // called, as the last argument of the xCreateTokenizer() function. + let constructorPointer = Unmanaged.passRetained(constructor).toOpaque() + + func deleteConstructor(constructorPointer: UnsafeMutableRawPointer?) { + guard let constructorPointer = constructorPointer else { return } + Unmanaged<AnyObject>.fromOpaque(constructorPointer).release() + } + + func xCreateTokenizer(constructorPointer: UnsafeMutableRawPointer?, azArg: UnsafeMutablePointer<UnsafePointer<Int8>?>?, nArg: Int32, tokenizerHandle: UnsafeMutablePointer<OpaquePointer?>?) -> Int32 { + guard let constructorPointer = constructorPointer else { + return SQLITE_ERROR + } + let constructor = Unmanaged<FTS5TokenizerConstructor>.fromOpaque(constructorPointer).takeUnretainedValue() + var arguments: [String] = [] + if let azArg = azArg { + for i in 0..<Int(nArg) { + if let cstr = azArg[i] { + arguments.append(String(cString: cstr)) + } + } + } + return constructor.constructor(constructor.db, arguments, tokenizerHandle) + } + + func xDeleteTokenizer(tokenizerPointer: OpaquePointer?) { + guard let tokenizerPointer = tokenizerPointer else { return } + Unmanaged<AnyObject>.fromOpaque(UnsafeMutableRawPointer(tokenizerPointer)).release() + } + + func xTokenize(tokenizerPointer: OpaquePointer?, context: UnsafeMutableRawPointer?, flags: Int32, pText: UnsafePointer<Int8>?, nText: Int32, tokenCallback: (@convention(c) (UnsafeMutableRawPointer?, Int32, UnsafePointer<Int8>?, Int32, Int32, Int32) -> Int32)?) -> Int32 { + guard let tokenizerPointer = tokenizerPointer else { + return SQLITE_ERROR + } + let object = Unmanaged<AnyObject>.fromOpaque(UnsafeMutableRawPointer(tokenizerPointer)).takeUnretainedValue() + guard let tokenizer = object as? FTS5Tokenizer else { + return SQLITE_ERROR + } + return tokenizer.tokenize(context: context, tokenization: FTS5Tokenization(rawValue: flags), pText: pText, nText: nText, tokenCallback: tokenCallback!) + } + + var xTokenizer = fts5_tokenizer(xCreate: xCreateTokenizer, xDelete: xDeleteTokenizer, xTokenize: xTokenize) + let code = withUnsafeMutablePointer(to: &xTokenizer) { xTokenizerPointer in + api.pointee.xCreateTokenizer(UnsafeMutablePointer(mutating: api), Tokenizer.name, constructorPointer, xTokenizerPointer, deleteConstructor) + } + guard code == SQLITE_OK else { + // Assume a GRDB bug: there is no point throwing any error. + fatalError(DatabaseError(resultCode: code, message: lastErrorMessage).description) + } + } + } + + extension DatabaseQueue { + + // MARK: - Custom FTS5 Tokenizers + + /// Add a custom FTS5 tokenizer. + /// + /// class MyTokenizer : FTS5CustomTokenizer { ... } + /// dbQueue.add(tokenizer: MyTokenizer.self) + public func add<Tokenizer: FTS5CustomTokenizer>(tokenizer: Tokenizer.Type) { + inDatabase { db in + db.add(tokenizer: Tokenizer.self) + } + } + } + + extension DatabasePool { + + // MARK: - Custom FTS5 Tokenizers + + /// Add a custom FTS5 tokenizer. + /// + /// class MyTokenizer : FTS5CustomTokenizer { ... } + /// dbPool.add(tokenizer: MyTokenizer.self) + public func add<Tokenizer: FTS5CustomTokenizer>(tokenizer: Tokenizer.Type) { + write { db in + db.add(tokenizer: Tokenizer.self) + } + } + } +#endif diff --git a/Pods/GRDB.swift/GRDB/FTS/FTS5Pattern.swift b/Pods/GRDB.swift/GRDB/FTS/FTS5Pattern.swift new file mode 100644 index 0000000..fa8e5a0 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/FTS/FTS5Pattern.swift @@ -0,0 +1,106 @@ +#if SQLITE_ENABLE_FTS5 + /// A full text pattern that can query FTS5 virtual tables. + public struct FTS5Pattern { + + /// The raw pattern string. Guaranteed to be a valid FTS5 pattern. + public let rawPattern: String + + /// Creates a pattern that matches any token found in the input string; + /// returns nil if no pattern could be built. + /// + /// FTS5Pattern(matchingAnyTokenIn: "") // nil + /// FTS5Pattern(matchingAnyTokenIn: "foo bar") // foo OR bar + /// + /// - parameter string: The string to turn into an FTS5 pattern + public init?(matchingAnyTokenIn string: String) { + guard let tokens = try? DatabaseQueue().inDatabase({ db in try db.makeTokenizer(.ascii()).nonSynonymTokens(in: string, for: .query) }) else { return nil } + guard !tokens.isEmpty else { return nil } + try? self.init(rawPattern: tokens.joined(separator: " OR ")) + } + + /// Creates a pattern that matches all tokens found in the input string; + /// returns nil if no pattern could be built. + /// + /// FTS5Pattern(matchingAllTokensIn: "") // nil + /// FTS5Pattern(matchingAllTokensIn: "foo bar") // foo bar + /// + /// - parameter string: The string to turn into an FTS5 pattern + public init?(matchingAllTokensIn string: String) { + guard let tokens = try? DatabaseQueue().inDatabase({ db in try db.makeTokenizer(.ascii()).nonSynonymTokens(in: string, for: .query) }) else { return nil } + guard !tokens.isEmpty else { return nil } + try? self.init(rawPattern: tokens.joined(separator: " ")) + } + + /// Creates a pattern that matches a contiguous string; returns nil if no + /// pattern could be built. + /// + /// FTS5Pattern(matchingPhrase: "") // nil + /// FTS5Pattern(matchingPhrase: "foo bar") // "foo bar" + /// + /// - parameter string: The string to turn into an FTS5 pattern + public init?(matchingPhrase string: String) { + guard let tokens = try? DatabaseQueue().inDatabase({ db in try db.makeTokenizer(.ascii()).nonSynonymTokens(in: string, for: .query) }) else { return nil } + guard !tokens.isEmpty else { return nil } + try? self.init(rawPattern: "\"" + tokens.joined(separator: " ") + "\"") + } + + init(rawPattern: String, allowedColumns: [String] = []) throws { + // Correctness above all: use SQLite to validate the pattern. + // + // Invalid patterns have SQLite return an error on the first + // call to sqlite3_step() on a statement that matches against + // that pattern. + do { + try DatabaseQueue().inDatabase { db in + try db.create(virtualTable: "documents", using: FTS5()) { t in + if allowedColumns.isEmpty { + t.column("__grdb__") + } else { + for column in allowedColumns { + t.column(column) + } + } + } + try db.makeSelectStatement("SELECT * FROM documents WHERE documents MATCH ?") + .cursor(arguments: [rawPattern]) + .next() // error on next() for invalid patterns + } + } catch let error as DatabaseError { + // Remove private SQL & arguments from the thrown error + throw DatabaseError(resultCode: error.extendedResultCode, message: error.message, sql: nil, arguments: nil) + } + + // Pattern is valid + self.rawPattern = rawPattern + } + } + + extension Database { + + /// Creates a pattern from a raw pattern string; throws DatabaseError on + /// invalid syntax. + /// + /// The pattern syntax is documented at https://www.sqlite.org/fts5.html#full_text_query_syntax + /// + /// try db.makeFTS5Pattern(rawPattern: "and", forTable: "documents") // OK + /// try db.makeFTS5Pattern(rawPattern: "AND", forTable: "documents") // malformed MATCH expression: [AND] + public func makeFTS5Pattern(rawPattern: String, forTable table: String) throws -> FTS5Pattern { + return try FTS5Pattern(rawPattern: rawPattern, allowedColumns: columns(in: table).map { $0.name }) + } + } + + extension FTS5Pattern : DatabaseValueConvertible { + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return rawPattern.databaseValue + } + + /// Returns an FTS5Pattern initialized from *dbValue*, if it + /// contains a suitable value. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> FTS5Pattern? { + return String + .fromDatabaseValue(dbValue) + .flatMap { try? FTS5Pattern(rawPattern: $0) } + } + } +#endif diff --git a/Pods/GRDB.swift/GRDB/FTS/FTS5Tokenizer.swift b/Pods/GRDB.swift/GRDB/FTS/FTS5Tokenizer.swift new file mode 100644 index 0000000..f57eeb8 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/FTS/FTS5Tokenizer.swift @@ -0,0 +1,213 @@ +#if SQLITE_ENABLE_FTS5 + /// A low-level SQLite function that lets FTS5Tokenizer notify tokens. + /// + /// See FTS5Tokenizer.tokenize(context:flags:pText:nText:tokenCallback:) + public typealias FTS5TokenCallback = @convention(c) (_ context: UnsafeMutableRawPointer?, _ flags: Int32, _ pToken: UnsafePointer<Int8>?, _ nToken: Int32, _ iStart: Int32, _ iEnd: Int32) -> Int32 + + /// The reason why FTS5 is requesting tokenization. + /// + /// See https://www.sqlite.org/fts5.html#custom_tokenizers + public struct FTS5Tokenization : OptionSet { + public let rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + /// FTS5_TOKENIZE_QUERY + public static let query = FTS5Tokenization(rawValue: FTS5_TOKENIZE_QUERY) + + /// FTS5_TOKENIZE_PREFIX + public static let prefix = FTS5Tokenization(rawValue: FTS5_TOKENIZE_PREFIX) + + /// FTS5_TOKENIZE_DOCUMENT + public static let document = FTS5Tokenization(rawValue: FTS5_TOKENIZE_DOCUMENT) + + /// FTS5_TOKENIZE_AUX + public static let aux = FTS5Tokenization(rawValue: FTS5_TOKENIZE_AUX) + } + + /// The protocol for FTS5 tokenizers + public protocol FTS5Tokenizer : class { + /// Tokenizes the text described by `pText` and `nText`, and + /// notifies found tokens to the `tokenCallback` function. + /// + /// It matches the `xTokenize` function documented at https://www.sqlite.org/fts5.html#custom_tokenizers + /// + /// - parameters: + /// - context: An opaque pointer that is the first argument to + /// the `tokenCallback` function + /// - tokenization: The reason why FTS5 is requesting tokenization. + /// - pText: The tokenized text bytes. May or may not be + /// nul-terminated. + /// - nText: The number of bytes in the tokenized text. + /// - tokenCallback: The function to call for each found token. + /// It matches the `xToken` callback at https://www.sqlite.org/fts5.html#custom_tokenizers + func tokenize(context: UnsafeMutableRawPointer?, tokenization: FTS5Tokenization, pText: UnsafePointer<Int8>?, nText: Int32, tokenCallback: @escaping FTS5TokenCallback) -> Int32 + } + + private class TokenizeContext { + var tokens: [(String, FTS5TokenFlags)] = [] + } + + extension FTS5Tokenizer { + + /// Tokenizes the string argument into an array of + /// (String, FTS5TokenFlags) pairs. + /// + /// let tokenizer = try db.makeTokenizer(.ascii()) + /// try tokenizer.tokenize("foo bar", for: .document) // [("foo", flags), ("bar", flags)] + /// + /// - parameter string: The string to tokenize + /// - parameter tokenization: The reason why tokenization is requested: + /// - .document: Tokenize like a document being inserted into an FTS table. + /// - .query: Tokenize like the search pattern of the MATCH operator. + /// - parameter tokenizer: A FTS5TokenizerDescriptor such as .ascii() + func tokenize(_ string: String, for tokenization: FTS5Tokenization) throws -> [(String, FTS5TokenFlags)] { + return try ContiguousArray(string.utf8).withUnsafeBufferPointer { buffer -> [(String, FTS5TokenFlags)] in + guard let addr = buffer.baseAddress else { + return [] + } + let pText = UnsafeMutableRawPointer(mutating: addr).assumingMemoryBound(to: Int8.self) + let nText = Int32(buffer.count) + + var context = TokenizeContext() + try withUnsafeMutablePointer(to: &context) { contextPointer in + let code = tokenize(context: UnsafeMutableRawPointer(contextPointer), tokenization: tokenization, pText: pText, nText: nText, tokenCallback: { (contextPointer, flags, pToken, nToken, iStart, iEnd) -> Int32 in + guard let contextPointer = contextPointer else { return SQLITE_ERROR } + + // Extract token + guard let token = pToken.flatMap({ String(data: Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: $0), count: Int(nToken), deallocator: .none), encoding: .utf8) }) else { + return SQLITE_OK + } + + let context = contextPointer.assumingMemoryBound(to: TokenizeContext.self).pointee + context.tokens.append((token, FTS5TokenFlags(rawValue: flags))) + + return SQLITE_OK + }) + if (code != SQLITE_OK) { + throw DatabaseError(resultCode: code) + } + } + return context.tokens + } + } + + func nonSynonymTokens(in string: String, for tokenization: FTS5Tokenization) throws -> [String] { + var tokens: [String] = [] + for (token, flags) in try tokenize(string, for: tokenization) { + if !flags.contains(.colocated) { + tokens.append(token) + } + } + return tokens + } + } + + extension Database { + /// MARK: - FTS5 Tokenizers + + /// Private type that makes a pre-registered FTS5 tokenizer available + /// through the FTS5Tokenizer protocol. + private final class FTS5RegisteredTokenizer : FTS5Tokenizer { + let xTokenizer: fts5_tokenizer + let tokenizerPointer: OpaquePointer + + init(xTokenizer: fts5_tokenizer, contextPointer: UnsafeMutableRawPointer?, arguments: [String]) throws { + guard let xCreate = xTokenizer.xCreate else { + throw DatabaseError(resultCode: .SQLITE_ERROR, message: "nil fts5_tokenizer.xCreate") + } + + self.xTokenizer = xTokenizer + + var tokenizerPointer: OpaquePointer? = nil + let code: Int32 + if let argument = arguments.first { + // Turn [String] into ContiguousArray<UnsafePointer<Int8>> + // (for an alternative implementation see https://oleb.net/blog/2016/10/swift-array-of-c-strings/) + func convertArguments<Result>(_ array: inout ContiguousArray<UnsafePointer<Int8>>, _ car: String, _ cdr: [String], _ body: (ContiguousArray<UnsafePointer<Int8>>) -> Result) -> Result { + return car.withCString { cString in + if let car = cdr.first { + array.append(cString) + return convertArguments(&array, car, Array(cdr.suffix(from: 1)), body) + } else { + return body(array) + } + } + } + var cStrings = ContiguousArray<UnsafePointer<Int8>>() + code = convertArguments(&cStrings, argument, Array(arguments.suffix(from: 1))) { cStrings in + cStrings.withUnsafeBufferPointer { azArg in + xCreate(contextPointer, UnsafeMutablePointer(OpaquePointer(azArg.baseAddress!)), Int32(cStrings.count), &tokenizerPointer) + } + } + } else { + code = xCreate(contextPointer, nil, 0, &tokenizerPointer) + } + + guard code == SQLITE_OK else { + throw DatabaseError(resultCode: code, message: "failed fts5_tokenizer.xCreate") + } + + if let tokenizerPointer = tokenizerPointer { + self.tokenizerPointer = tokenizerPointer + } else { + throw DatabaseError(resultCode: code, message: "nil tokenizer") + } + } + + deinit { + if let delete = xTokenizer.xDelete { + delete(tokenizerPointer) + } + } + + func tokenize(context: UnsafeMutableRawPointer?, tokenization: FTS5Tokenization, pText: UnsafePointer<Int8>?, nText: Int32, tokenCallback: @escaping FTS5TokenCallback) -> Int32 { + guard let xTokenize = xTokenizer.xTokenize else { + return SQLITE_ERROR + } + return xTokenize(tokenizerPointer, context, tokenization.rawValue, pText, nText, tokenCallback) + } + } + + /// Creates an FTS5 tokenizer, given its descriptor. + /// + /// let unicode61 = try db.makeTokenizer(.unicode61()) + /// + /// It is a programmer error to use the tokenizer outside of a protected + /// database queue, or after the database has been closed. + /// + /// Use this method when you implement a custom wrapper tokenizer: + /// + /// final class MyTokenizer : FTS5WrapperTokenizer { + /// var wrappedTokenizer: FTS5Tokenizer + /// + /// init(db: Database, arguments: [String]) throws { + /// wrappedTokenizer = try db.makeTokenizer(.unicode61()) + /// } + /// } + public func makeTokenizer(_ descriptor: FTS5TokenizerDescriptor) throws -> FTS5Tokenizer { + let api = FTS5.api(self) + + let xTokenizerPointer: UnsafeMutablePointer<fts5_tokenizer> = .allocate(capacity: 1) + defer { xTokenizerPointer.deallocate(capacity: 1) } + + let contextHandle: UnsafeMutablePointer<UnsafeMutableRawPointer?> = .allocate(capacity: 1) + defer { contextHandle.deallocate(capacity: 1) } + + let code = api.pointee.xFindTokenizer!( + UnsafeMutablePointer(mutating: api), + descriptor.name, + contextHandle, + xTokenizerPointer) + + guard code == SQLITE_OK else { + throw DatabaseError(resultCode: code) + } + + let contextPointer = contextHandle.pointee + return try FTS5RegisteredTokenizer(xTokenizer: xTokenizerPointer.pointee, contextPointer: contextPointer, arguments: descriptor.arguments) + } + } +#endif diff --git a/Pods/GRDB.swift/GRDB/FTS/FTS5TokenizerDescriptor.swift b/Pods/GRDB.swift/GRDB/FTS/FTS5TokenizerDescriptor.swift new file mode 100644 index 0000000..9752d9d --- /dev/null +++ b/Pods/GRDB.swift/GRDB/FTS/FTS5TokenizerDescriptor.swift @@ -0,0 +1,117 @@ +#if SQLITE_ENABLE_FTS5 + /// An FTS5 tokenizer, suitable for FTS5 table definitions: + /// + /// db.create(virtualTable: "books", using: FTS5()) { t in + /// t.tokenizer = .unicode61() // FTS5TokenizerDescriptor + /// } + /// + /// See https://www.sqlite.org/fts5.html#tokenizers + public struct FTS5TokenizerDescriptor { + /// The tokenizer components + /// + /// // ["unicode61"] + /// FTS5TokenizerDescriptor.unicode61().components + /// + /// // ["unicode61", "remove_diacritics", "0"] + /// FTS5TokenizerDescriptor.unicode61(removeDiacritics: false)).components + public let components: [String] + + /// The tokenizer name + /// + /// // "unicode61" + /// FTS5TokenizerDescriptor.unicode61().name + /// + /// // "unicode61" + /// FTS5TokenizerDescriptor.unicode61(removeDiacritics: false)).name + var name: String { + return components[0] + } + + var arguments: [String] { + return Array(components.suffix(from: 1)) + } + + /// Creates an FTS5 tokenizer descriptor. + /// + /// db.create(virtualTable: "books", using: FTS5()) { t in + /// let tokenizer = FTS5TokenizerDescriptor(components: ["porter", "unicode61", "remove_diacritics", "0"]) + /// t.tokenizer = tokenizer + /// } + /// + /// - precondition: Components is not empty + public init(components: [String]) { + GRDBPrecondition(!components.isEmpty, "FTS5TokenizerDescriptor requires at least one component") + assert(!components.isEmpty) + self.components = components + } + + /// The "ascii" tokenizer + /// + /// db.create(virtualTable: "books", using: FTS5()) { t in + /// t.tokenizer = .ascii() + /// } + /// + /// - parameters: + /// - separators: Unless empty (the default), SQLite will consider + /// these characters as token separators. + /// + /// See https://www.sqlite.org/fts5.html#ascii_tokenizer + public static func ascii(separators: Set<Character> = []) -> FTS5TokenizerDescriptor { + if separators.isEmpty { + return FTS5TokenizerDescriptor(components: ["ascii"]) + } else { + return FTS5TokenizerDescriptor(components: ["ascii", "separators", separators.map { String($0) }.joined(separator: "").sqlExpression.sql]) + } + } + + /// The "porter" tokenizer + /// + /// db.create(virtualTable: "books", using: FTS5()) { t in + /// t.tokenizer = .porter() + /// } + /// + /// - parameters: + /// - base: An eventual wrapping tokenizer which replaces the + // default unicode61() base tokenizer. + /// + /// See https://www.sqlite.org/fts5.html#porter_tokenizer + public static func porter(wrapping base: FTS5TokenizerDescriptor? = nil) -> FTS5TokenizerDescriptor { + if let base = base { + return FTS5TokenizerDescriptor(components: ["porter"] + base.components) + } else { + return FTS5TokenizerDescriptor(components: ["porter"]) + } + } + + /// An "unicode61" tokenizer + /// + /// db.create(virtualTable: "books", using: FTS5()) { t in + /// t.tokenizer = .unicode61() + /// } + /// + /// - parameters: + /// - removeDiacritics: If true (the default), then SQLite will + /// strip diacritics from latin characters. + /// - separators: Unless empty (the default), SQLite will consider + /// these characters as token separators. + /// - tokenCharacters: Unless empty (the default), SQLite will + /// consider these characters as token characters. + /// + /// See https://www.sqlite.org/fts5.html#unicode61_tokenizer + public static func unicode61(removeDiacritics: Bool = true, separators: Set<Character> = [], tokenCharacters: Set<Character> = []) -> FTS5TokenizerDescriptor { + var components: [String] = ["unicode61"] + if !removeDiacritics { + components.append(contentsOf: ["remove_diacritics", "0"]) + } + if !separators.isEmpty { + // TODO: test "=" and "\"", "(" and ")" as separators, with both FTS3Pattern(matchingAnyTokenIn:tokenizer:) and Database.create(virtualTable:using:) + components.append(contentsOf: ["separators", separators.sorted().map { String($0) }.joined(separator: "").sqlExpression.sql]) + } + if !tokenCharacters.isEmpty { + // TODO: test "=" and "\"", "(" and ")" as tokenCharacters, with both FTS3Pattern(matchingAnyTokenIn:tokenizer:) and Database.create(virtualTable:using:) + components.append(contentsOf: ["tokenchars", tokenCharacters.sorted().map { String($0) }.joined(separator: "").sqlExpression.sql]) + } + return FTS5TokenizerDescriptor(components: components) + } + } +#endif diff --git a/Pods/GRDB.swift/GRDB/FTS/FTS5WrapperTokenizer.swift b/Pods/GRDB.swift/GRDB/FTS/FTS5WrapperTokenizer.swift new file mode 100644 index 0000000..65b683f --- /dev/null +++ b/Pods/GRDB.swift/GRDB/FTS/FTS5WrapperTokenizer.swift @@ -0,0 +1,141 @@ +#if SQLITE_ENABLE_FTS5 + /// Flags that tell SQLite how to register a token. + /// + /// See https://www.sqlite.org/fts5.html#custom_tokenizers + public struct FTS5TokenFlags : OptionSet { + public let rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + /// FTS5_TOKEN_COLOCATED + public static let colocated = FTS5TokenFlags(rawValue: FTS5_TOKEN_COLOCATED) + } + + /// A function that lets FTS5WrapperTokenizer notify tokens. + /// + /// See FTS5WrapperTokenizer.accept(token:flags:tokenCallback:) + public typealias FTS5WrapperTokenCallback = (_ token: String, _ flags: FTS5TokenFlags) throws -> () + + /// The protocol for custom FTS5 tokenizers that wrap another tokenizer. + /// + /// Types that adopt FTS5WrapperTokenizer don't have to implement the + /// low-level FTS5Tokenizer.tokenize(context:flags:pText:nText:tokenCallback:). + /// + /// Instead, they process regular Swift strings. + /// + /// Here is the implementation for a trivial tokenizer that wraps the + /// built-in ascii tokenizer without any custom processing: + /// + /// class TrivialAsciiTokenizer : FTS5WrapperTokenizer { + /// static let name = "trivial" + /// let wrappedTokenizer: FTS5Tokenizer + /// + /// init(db: Database, arguments: [String]) throws { + /// wrappedTokenizer = try db.makeTokenizer(.ascii()) + /// } + /// + /// func accept(token: String, flags: FTS5TokenFlags, for tokenization: FTS5Tokenization, tokenCallback: FTS5WrapperTokenCallback) throws { + /// try tokenCallback(token, flags) + /// } + /// } + public protocol FTS5WrapperTokenizer : FTS5CustomTokenizer { + /// The wrapped tokenizer + var wrappedTokenizer: FTS5Tokenizer { get } + + /// Given a token produced by the wrapped tokenizer, notifies customized + /// tokens to the `tokenCallback` function. + /// + /// For example: + /// + /// func accept(token: String, flags: FTS5TokenFlags, for tokenization: FTS5Tokenization, tokenCallback: FTS5WrapperTokenCallback) throws { + /// // pass through: + /// try tokenCallback(token, flags) + /// } + /// + /// When implementing the accept method, there are a two rules + /// to observe: + /// + /// 1. Errors thrown by the tokenCallback function must not be caught. + /// + /// 2. The input `flags` should be given unmodified to the tokenCallback + /// function, unless you union it with the .colocated flag when the + /// tokenizer produces synonyms (see + /// https://www.sqlite.org/fts5.html#synonym_support). + /// + /// - parameters: + /// - token: A token produced by the wrapped tokenizer + /// - flags: Flags that tell SQLite how to register a token. + /// - tokenization: The reason why FTS5 is requesting tokenization. + /// - tokenCallback: The function to call for each customized token. + func accept(token: String, flags: FTS5TokenFlags, for tokenization: FTS5Tokenization, tokenCallback: FTS5WrapperTokenCallback) throws + } + + private struct FTS5WrapperContext { + let tokenizer: FTS5WrapperTokenizer + let context: UnsafeMutableRawPointer? + let tokenization: FTS5Tokenization + let tokenCallback: FTS5TokenCallback + } + + extension FTS5WrapperTokenizer { + /// Default implementation + public func tokenize(context: UnsafeMutableRawPointer?, tokenization: FTS5Tokenization, pText: UnsafePointer<Int8>?, nText: Int32, tokenCallback: @escaping FTS5TokenCallback) -> Int32 { + // `tokenCallback` is @convention(c). This requires a little setup + // in order to transfer context. + var customContext = FTS5WrapperContext( + tokenizer: self, + context: context, + tokenization: tokenization, + tokenCallback: tokenCallback) + return withUnsafeMutablePointer(to: &customContext) { customContextPointer in + // Invoke wrappedTokenizer + return wrappedTokenizer.tokenize(context: customContextPointer, tokenization: tokenization, pText: pText, nText: nText) { (customContextPointer, tokenFlags, pToken, nToken, iStart, iEnd) in + + // Extract token produced by wrapped tokenizer + guard let token = pToken.flatMap({ String(data: Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: $0), count: Int(nToken), deallocator: .none), encoding: .utf8) }) else { + return 0 // SQLITE_OK + } + + // Extract context + let customContext = customContextPointer!.assumingMemoryBound(to: FTS5WrapperContext.self).pointee + let tokenizer = customContext.tokenizer + let context = customContext.context + let tokenization = customContext.tokenization + let tokenCallback = customContext.tokenCallback + + // Process token produced by wrapped tokenizer + do { + try tokenizer.accept( + token: token, + flags: FTS5TokenFlags(rawValue: tokenFlags), + for: tokenization, + tokenCallback: { (token, flags) in + // Turn token into bytes + return try ContiguousArray(token.utf8).withUnsafeBufferPointer { buffer in + guard let addr = buffer.baseAddress else { + return + } + let pToken = UnsafeMutableRawPointer(mutating: addr).assumingMemoryBound(to: Int8.self) + let nToken = Int32(buffer.count) + + // Inject token bytes into SQLite + let code = tokenCallback(context, flags.rawValue, pToken, nToken, iStart, iEnd) + guard code == SQLITE_OK else { + throw DatabaseError(resultCode: code, message: "token callback failed") + } + } + }) + + return SQLITE_OK + } catch let error as DatabaseError { + return error.extendedResultCode.rawValue + } catch { + return SQLITE_ERROR + } + } + } + } + } +#endif diff --git a/Pods/GRDB.swift/GRDB/Legacy/Fixits-0-84-0.swift b/Pods/GRDB.swift/GRDB/Legacy/Fixits-0-84-0.swift new file mode 100644 index 0000000..09caa91 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Legacy/Fixits-0-84-0.swift @@ -0,0 +1,19 @@ +import Foundation + +@available(*, unavailable, renamed:"Database.ForeignKeyAction") +public typealias SQLForeignKeyAction = Database.ForeignKeyAction + +@available(*, unavailable, renamed:"Database.ColumnType") +public typealias SQLColumnType = Database.ColumnType + +@available(*, unavailable, renamed:"Database.ConflictResolution") +public typealias SQLConflictResolution = Database.ConflictResolution + +@available(*, unavailable, renamed:"Database.CollationName") +public typealias SQLCollation = Database.CollationName + +@available(*, unavailable, renamed:"SQLSpecificExpressible") +public typealias _SpecificSQLExpressible = SQLSpecificExpressible + +@available(*, unavailable, renamed:"SQLExpression") +public typealias _SQLExpression = SQLExpression diff --git a/Pods/GRDB.swift/GRDB/Legacy/Fixits-0-90-1.swift b/Pods/GRDB.swift/GRDB/Legacy/Fixits-0-90-1.swift new file mode 100644 index 0000000..681937a --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Legacy/Fixits-0-90-1.swift @@ -0,0 +1,55 @@ +extension Row { + @available(*, unavailable, renamed:"fetchCursor") + public static func fetch(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) -> Any { preconditionFailure() } + @available(*, unavailable, renamed:"fetchCursor") + public static func fetch(_ db: Database, _ request: Request) -> Any { preconditionFailure() } + @available(*, unavailable, renamed:"fetchCursor") + public static func fetch(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) -> Any { preconditionFailure() } +} + +extension DatabaseValueConvertible { + @available(*, unavailable, renamed:"fetchCursor") + public static func fetch(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) -> Any { preconditionFailure() } + @available(*, unavailable, renamed:"fetchCursor") + public static func fetch(_ db: Database, _ request: Request) -> Any { preconditionFailure() } + @available(*, unavailable, renamed:"fetchCursor") + public static func fetch(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) -> Any { preconditionFailure() } +} + +extension Optional where Wrapped: DatabaseValueConvertible { + @available(*, unavailable, renamed:"fetchCursor") + public static func fetch(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) -> Any { preconditionFailure() } + @available(*, unavailable, renamed:"fetchCursor") + public static func fetch(_ db: Database, _ request: Request) -> Any { preconditionFailure() } + @available(*, unavailable, renamed:"fetchCursor") + public static func fetch(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) -> Any { preconditionFailure() } +} + +extension QueryInterfaceRequest { + @available(*, unavailable, renamed:"fetchCursor") + public func fetch(_ db: Database) -> Any { preconditionFailure() } +} + +extension RowConvertible { + @available(*, unavailable, renamed:"fetchCursor") + public static func fetch(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) -> Any { preconditionFailure() } + @available(*, unavailable, renamed:"fetchCursor") + public static func fetch(_ db: Database, _ request: Request) -> Any { preconditionFailure() } + @available(*, unavailable, renamed:"fetchCursor") + public static func fetch(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) -> Any { preconditionFailure() } +} + +extension RowConvertible where Self: TableMapping { + @available(*, unavailable, renamed:"fetchCursor") + public static func fetch(_ db: Database) -> Any { preconditionFailure() } + @available(*, unavailable, renamed:"fetchCursor") + public static func fetch<Sequence: Swift.Sequence>(_ db: Database, keys: Sequence) -> Any where Sequence.Iterator.Element: DatabaseValueConvertible { preconditionFailure() } + @available(*, unavailable, renamed:"fetchCursor") + public static func fetch(_ db: Database, keys: [[String: DatabaseValueConvertible?]]) -> Any { preconditionFailure() } +} + +@available(*, unavailable, message:"DatabaseSequence has been replaced by Cursor.") +public struct DatabaseSequence<T> { } + +@available(*, unavailable, message:"DatabaseIterator has been replaced by Cursor.") +public struct DatabaseIterator<T> { } diff --git a/Pods/GRDB.swift/GRDB/Legacy/Fixits-0.101.1.swift b/Pods/GRDB.swift/GRDB/Legacy/Fixits-0.101.1.swift new file mode 100644 index 0000000..78c6071 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Legacy/Fixits-0.101.1.swift @@ -0,0 +1,4 @@ +extension DatabaseError { + @available(*, unavailable, renamed:"resultCode") + public var code: Int32 { return 0 } +} diff --git a/Pods/GRDB.swift/GRDB/Legacy/Fixits-0.102.0.swift b/Pods/GRDB.swift/GRDB/Legacy/Fixits-0.102.0.swift new file mode 100644 index 0000000..379703b --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Legacy/Fixits-0.102.0.swift @@ -0,0 +1,9 @@ +extension Database { + @available(*, unavailable, renamed:"inTransaction") + public func writeInTransaction(_ kind: Database.TransactionKind? = nil, _ block: (Database) throws -> Database.TransactionCompletion) throws { } +} + +extension DatabaseValue { + @available(*, unavailable, message:"DatabaseSequence has been replaced by Cursor.") + public func value() -> Any { preconditionFailure() } +} diff --git a/Pods/GRDB.swift/GRDB/Legacy/Fixits-0.109.0.swift b/Pods/GRDB.swift/GRDB/Legacy/Fixits-0.109.0.swift new file mode 100644 index 0000000..d26a5d5 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Legacy/Fixits-0.109.0.swift @@ -0,0 +1,4 @@ +extension Request { + @available(*, unavailable, renamed:"asRequest(of:)") + public func bound<T>(to type: T.Type) -> AnyTypedRequest<T> { preconditionFailure() } +} diff --git a/Pods/GRDB.swift/GRDB/Legacy/Fixits-1.2.swift b/Pods/GRDB.swift/GRDB/Legacy/Fixits-1.2.swift new file mode 100644 index 0000000..97857d0 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Legacy/Fixits-1.2.swift @@ -0,0 +1,28 @@ +extension Row { + @available(*, unavailable, message:"use subscript instead: row[index]") + public func value(atIndex index: Int) -> DatabaseValueConvertible? { preconditionFailure() } + + @available(*, unavailable, message:"use subscript instead: row[index]") + public func value<Value: DatabaseValueConvertible>(atIndex index: Int) -> Value? { preconditionFailure() } + + @available(*, unavailable, message:"use subscript instead: row[index]") + public func value<Value: DatabaseValueConvertible>(atIndex index: Int) -> Value { preconditionFailure() } + + @available(*, unavailable, message:"use subscript instead: row[column]") + public func value(named name: String) -> DatabaseValueConvertible? { preconditionFailure() } + + @available(*, unavailable, message:"use subscript instead: row[column]") + public func value<Value: DatabaseValueConvertible>(named name: String) -> Value? { preconditionFailure() } + + @available(*, unavailable, message:"use subscript instead: row[column]") + public func value<Value: DatabaseValueConvertible>(named name: String) -> Value { preconditionFailure() } + + @available(*, unavailable, message:"use subscript instead: row[column]") + public func value(_ column: Column) -> DatabaseValueConvertible? { preconditionFailure() } + + @available(*, unavailable, message:"use subscript instead: row[column]") + public func value<Value: DatabaseValueConvertible>(_ column: Column) -> Value? { preconditionFailure() } + + @available(*, unavailable, message:"use subscript instead: row[column]") + public func value<Value: DatabaseValueConvertible>(_ column: Column) -> Value { preconditionFailure() } +} diff --git a/Pods/GRDB.swift/GRDB/Legacy/Fixits-Swift2.swift b/Pods/GRDB.swift/GRDB/Legacy/Fixits-Swift2.swift new file mode 100644 index 0000000..f8a21f2 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Legacy/Fixits-Swift2.swift @@ -0,0 +1,208 @@ +// +// Fixits-Swift2.swift +// GRDB +// +// Created by Swiftlyfalling. +// +// Provides automatic renaming Fix-Its for many of the Swift 2.x -> Swift 3 GRDB API changes. +// Consult the CHANGELOG.md and documentation for details on all of the changes. +// + +import Foundation +#if os(iOS) + import UIKit +#endif + +// Database Connections + +@available(*, unavailable, renamed:"Database.BusyMode") +public typealias BusyMode = Database.BusyMode + +@available(*, unavailable, renamed:"Database.CheckpointMode") +public typealias CheckpointMode = Database.CheckpointMode + +@available(*, unavailable, renamed:"Database.TransactionKind") +public typealias TransactionKind = Database.TransactionKind + +@available(*, unavailable, renamed:"Database.TransactionCompletion") +public typealias TransactionCompletion = Database.TransactionCompletion + +@available(*, unavailable, renamed:"Database.BusyCallback") +public typealias BusyCallback = Database.BusyCallback + +extension DatabasePool { +#if os(iOS) + @available(*, unavailable, renamed:"setupMemoryManagement(in:)") + public func setupMemoryManagement(application: UIApplication) { } +#endif +#if SQLITE_HAS_CODEC + @available(*, unavailable, renamed:"change(passphrase:)") + public func changePassphrase(_ passphrase: String) throws { } +#endif +} + +extension DatabaseQueue { +#if os(iOS) + @available(*, unavailable, renamed:"setupMemoryManagement(in:)") + public func setupMemoryManagement(application: UIApplication) { } +#endif +#if SQLITE_HAS_CODEC + @available(*, unavailable, renamed:"change(passphrase:)") + public func changePassphrase(_ passphrase: String) throws { } +#endif +} + +// SQL Functions + +extension Database { + @available(*, unavailable, renamed:"add(function:)") + public func addFunction(_ function: DatabaseFunction) { } + + @available(*, unavailable, renamed:"remove(function:)") + public func removeFunction(_ function: DatabaseFunction) { } +} + +extension DatabasePool { + @available(*, unavailable, renamed:"add(function:)") + public func addFunction(_ function: DatabaseFunction) { } + + @available(*, unavailable, renamed:"remove(function:)") + public func removeFunction(_ function: DatabaseFunction) { } +} + +extension DatabaseQueue { + @available(*, unavailable, renamed:"add(function:)") + public func addFunction(_ function: DatabaseFunction) { } + + @available(*, unavailable, renamed:"remove(function:)") + public func removeFunction(_ function: DatabaseFunction) { } +} + +extension DatabaseReader { + @available(*, unavailable, renamed:"add(function:)") + public func addFunction(_ function: DatabaseFunction) { } + + @available(*, unavailable, renamed:"remove(function:)") + public func removeFunction(_ function: DatabaseFunction) { } +} + +extension DatabaseFunction { + @available(*, unavailable, renamed:"capitalize") + public static let capitalizedString = capitalize + + @available(*, unavailable, renamed:"lowercase") + public static let lowercaseString = lowercase + + @available(*, unavailable, renamed:"uppercase") + public static let uppercaseString = uppercase +} + +@available(iOS 9.0, OSX 10.11, watchOS 3.0, *) +extension DatabaseFunction { + @available(*, unavailable, renamed:"localizedCapitalize") + public static let localizedCapitalizedString = localizedCapitalize + + @available(*, unavailable, renamed:"localizedLowercase") + public static let localizedLowercaseString = localizedLowercase + + @available(*, unavailable, renamed:"localizedUppercase") + public static let localizedUppercaseString = localizedUppercase +} + +// SQL Collations + +extension Database { + @available(*, unavailable, renamed:"add(collation:)") + public func addCollation(_ collation: DatabaseCollation) { } + + @available(*, unavailable, renamed:"remove(collation:)") + public func removeCollation(_ collation: DatabaseCollation) { } +} + +extension DatabasePool { + @available(*, unavailable, renamed:"add(collation:)") + public func addCollation(_ collation: DatabaseCollation) { } + + @available(*, unavailable, renamed:"remove(collation:)") + public func removeCollation(_ collation: DatabaseCollation) { } +} + +extension DatabaseQueue { + @available(*, unavailable, renamed:"add(collation:)") + public func addCollation(_ collation: DatabaseCollation) { } + + @available(*, unavailable, renamed:"remove(collation:)") + public func removeCollation(_ collation: DatabaseCollation) { } +} + +extension DatabaseReader { + @available(*, unavailable, renamed:"add(collation:)") + public func addCollation(_ collation: DatabaseCollation) { } + + @available(*, unavailable, renamed:"remove(collation:)") + public func removeCollation(_ collation: DatabaseCollation) { } +} + +// Prepared Statements + +extension Database { + @available(*, unavailable, renamed:"makeSelectStatement(_:)") + func selectStatement(_ sql: String) throws -> SelectStatement { preconditionFailure() } + + @available(*, unavailable, renamed:"makeUpdateStatement(_:)") + func updateStatement(_ sql: String) throws -> UpdateStatement { preconditionFailure() } +} + +extension Statement { + @available(*, unavailable, renamed:"validate(arguments:)") + public func validateArguments(_ arguments: StatementArguments) throws { } +} + +// Transaction Observers + +extension Database { + @available(*, unavailable, message:"Use add(transactionObserver:) instead. Database events filtering is now performed by transaction observers themselves.") + public func addTransactionObserver(_ transactionObserver: TransactionObserver, forDatabaseEvents filter: ((DatabaseEventKind) -> Bool)? = nil) { } + + @available(*, unavailable, renamed:"remove(transactionObserver:)") + public func removeTransactionObserver(_ transactionObserver: TransactionObserver) { } +} + +extension DatabaseWriter { + @available(*, unavailable, message:"Use add(transactionObserver:) instead. Database events filtering is now performed by transaction observers themselves.") + public func addTransactionObserver(_ transactionObserver: TransactionObserver, forDatabaseEvents filter: ((DatabaseEventKind) -> Bool)? = nil) { } + + @available(*, unavailable, renamed:"remove(transactionObserver:)") + public func removeTransactionObserver(_ transactionObserver: TransactionObserver) { } +} + +@available(*, unavailable, renamed:"TransactionObserver") +public typealias TransactionObserverType = TransactionObserver + +// Query Interface + +@available(*, unavailable, renamed:"Column") +public typealias SQLColumn = Column + +extension SQLSpecificExpressible { + @available(*, unavailable, renamed:"capitalized") + public var capitalizedString: SQLExpression { get { return capitalized } } + + @available(*, unavailable, renamed:"lowercased") + public var lowercaseString: SQLExpression { get { return lowercased } } + + @available(*, unavailable, renamed:"uppercased") + public var uppercaseString: SQLExpression { get { return uppercased } } +} + +@available(iOS 9.0, OSX 10.11, watchOS 3.0, *) +extension SQLSpecificExpressible { + @available(*, unavailable, renamed:"localizedCapitalized") + public var localizedCapitalizedString: SQLExpression { get { return localizedCapitalized } } + + @available(*, unavailable, renamed:"localizedLowercased") + public var localizedLowercaseString: SQLExpression { get { return localizedLowercased } } + + @available(*, unavailable, renamed:"localizedUppercased") + public var localizedUppercaseString: SQLExpression { get { return localizedUppercased } } +} diff --git a/Pods/GRDB.swift/GRDB/Migration/DatabaseMigrator.swift b/Pods/GRDB.swift/GRDB/Migration/DatabaseMigrator.swift new file mode 100644 index 0000000..0d43334 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Migration/DatabaseMigrator.swift @@ -0,0 +1,195 @@ +/// A DatabaseMigrator registers and applies database migrations. +/// +/// Migrations are named blocks of SQL statements that are guaranteed to be +/// applied in order, once and only once. +/// +/// When a user upgrades your application, only non-applied migration are run. +/// +/// Usage: +/// +/// var migrator = DatabaseMigrator() +/// +/// // v1.0 database +/// migrator.registerMigration("createAuthors") { db in +/// try db.execute(""" +/// CREATE TABLE authors ( +/// id INTEGER PRIMARY KEY, +/// creationDate TEXT, +/// name TEXT NOT NULL +/// ) +/// """) +/// } +/// +/// migrator.registerMigration("createBooks") { db in +/// try db.execute(""" +/// CREATE TABLE books ( +/// uuid TEXT PRIMARY KEY, +/// authorID INTEGER NOT NULL +/// REFERENCES authors(id) +/// ON DELETE CASCADE ON UPDATE CASCADE, +/// title TEXT NOT NULL +/// ) +/// """) +/// } +/// +/// // v2.0 database +/// migrator.registerMigration("AddBirthYearToAuthors") { db in +/// try db.execute("ALTER TABLE authors ADD COLUMN birthYear INT") +/// } +/// +/// try migrator.migrate(dbQueue) +public struct DatabaseMigrator { + + /// A new migrator. + public init() { + } + + /// Registers a migration. + /// + /// migrator.registerMigration("createPlayers") { db in + /// try db.execute(""" + /// CREATE TABLE players ( + /// id INTEGER PRIMARY KEY, + /// creationDate TEXT, + /// name TEXT NOT NULL + /// ) + /// """) + /// } + /// + /// - parameters: + /// - identifier: The migration identifier. + /// - block: The migration block that performs SQL statements. + /// - precondition: No migration with the same same as already been registered. + public mutating func registerMigration(_ identifier: String, migrate: @escaping (Database) throws -> Void) { + registerMigration(Migration(identifier: identifier, migrate: migrate)) + } + + #if GRDBCUSTOMSQLITE || GRDBCIPHER + /// Registers an advanced migration, as described at https://www.sqlite.org/lang_altertable.html#otheralter + /// + /// // Add a NOT NULL constraint on players.name: + /// migrator.registerMigrationWithDeferredForeignKeyCheck("AddNotNullCheckOnName") { db in + /// try db.execute(""" + /// CREATE TABLE new_players (id INTEGER PRIMARY KEY, name TEXT NOT NULL); + /// INSERT INTO new_players SELECT * FROM players; + /// DROP TABLE players; + /// ALTER TABLE new_players RENAME TO players; + /// """) + /// } + /// + /// While your migration code runs with disabled foreign key checks, those + /// are re-enabled and checked at the end of the migration, regardless of + /// eventual errors. + /// + /// - parameters: + /// - identifier: The migration identifier. + /// - block: The migration block that performs SQL statements. + /// - precondition: No migration with the same same as already been registered. + public mutating func registerMigrationWithDeferredForeignKeyCheck(_ identifier: String, migrate: @escaping (Database) throws -> Void) { + registerMigration(Migration(identifier: identifier, disabledForeignKeyChecks: true, migrate: migrate)) + } + #else + @available(iOS 8.2, OSX 10.10, *) + /// Registers an advanced migration, as described at https://www.sqlite.org/lang_altertable.html#otheralter + /// + /// // Add a NOT NULL constraint on players.name: + /// migrator.registerMigrationWithDeferredForeignKeyCheck("AddNotNullCheckOnName") { db in + /// try db.execute(""" + /// CREATE TABLE new_players (id INTEGER PRIMARY KEY, name TEXT NOT NULL); + /// INSERT INTO new_players SELECT * FROM players; + /// DROP TABLE players; + /// ALTER TABLE new_players RENAME TO players; + /// """) + /// } + /// + /// While your migration code runs with disabled foreign key checks, those + /// are re-enabled and checked at the end of the migration, regardless of + /// eventual errors. + /// + /// - parameters: + /// - identifier: The migration identifier. + /// - block: The migration block that performs SQL statements. + /// - precondition: No migration with the same same as already been registered. + public mutating func registerMigrationWithDeferredForeignKeyCheck(_ identifier: String, migrate: @escaping (Database) throws -> Void) { + registerMigration(Migration(identifier: identifier, disabledForeignKeyChecks: true, migrate: migrate)) + } + #endif + + /// Iterate migrations in the same order as they were registered. If a + /// migration has not yet been applied, its block is executed in + /// a transaction. + /// + /// - parameter db: A DatabaseWriter (DatabaseQueue or DatabasePool) where + /// migrations should apply. + /// - throws: An eventual error thrown by the registered migration blocks. + public func migrate(_ writer: DatabaseWriter) throws { + try writer.write { db in + try setupMigrations(db) + try runMigrations(db) + } + } + + /// Iterate migrations in the same order as they were registered, up to the + /// provided target. If a migration has not yet been applied, its block is + /// executed in a transaction. + /// + /// - parameter db: A DatabaseWriter (DatabaseQueue or DatabasePool) where + /// migrations should apply. + /// - targetIdentifier: The identifier of a registered migration. + /// - throws: An eventual error thrown by the registered migration blocks. + public func migrate(_ writer: DatabaseWriter, upTo targetIdentifier: String) throws { + try writer.write { db in + try setupMigrations(db) + try runMigrations(db, upTo: targetIdentifier) + } + } + + + // MARK: - Non public + + private var migrations: [Migration] = [] + + private mutating func registerMigration(_ migration: Migration) { + GRDBPrecondition(!migrations.map({ $0.identifier }).contains(migration.identifier), "already registered migration: \(String(reflecting: migration.identifier))") + migrations.append(migration) + } + + private func setupMigrations(_ db: Database) throws { + try db.execute("CREATE TABLE IF NOT EXISTS grdb_migrations (identifier TEXT NOT NULL PRIMARY KEY)") + } + + private func appliedIdentifiers(_ db: Database) throws -> Set<String> { + return try Set(String.fetchAll(db, "SELECT identifier FROM grdb_migrations")) + } + + private func runMigrations(_ db: Database) throws { + let appliedIdentifiers = try self.appliedIdentifiers(db) + for migration in migrations where !appliedIdentifiers.contains(migration.identifier) { + try migration.run(db) + } + } + + private func runMigrations(_ db: Database, upTo targetIdentifier: String) throws { + var prefixMigrations: [Migration] = [] + for migration in migrations { + prefixMigrations.append(migration) + if migration.identifier == targetIdentifier { + break + } + } + + // targetIdentifier must refer to a registered migration + GRDBPrecondition(prefixMigrations.last?.identifier == targetIdentifier, "undefined migration: \(String(reflecting: targetIdentifier))") + + // Subsequent migration must not be applied + let appliedIdentifiers = try self.appliedIdentifiers(db) + if prefixMigrations.count < migrations.count { + let nextIdentifier = migrations[prefixMigrations.count].identifier + GRDBPrecondition(!appliedIdentifiers.contains(nextIdentifier), "database is already migrated beyond migration \(String(reflecting: targetIdentifier))") + } + + for migration in prefixMigrations where !appliedIdentifiers.contains(migration.identifier) { + try migration.run(db) + } + } +} diff --git a/Pods/GRDB.swift/GRDB/Migration/Migration.swift b/Pods/GRDB.swift/GRDB/Migration/Migration.swift new file mode 100644 index 0000000..8b54d9d --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Migration/Migration.swift @@ -0,0 +1,85 @@ +/// An internal struct that defines a migration. +struct Migration { + let identifier: String + let disabledForeignKeyChecks: Bool + let migrate: (Database) throws -> Void + + #if GRDBCUSTOMSQLITE || GRDBCIPHER + init(identifier: String, disabledForeignKeyChecks: Bool = false, migrate: @escaping (Database) throws -> Void) { + self.identifier = identifier + self.disabledForeignKeyChecks = disabledForeignKeyChecks + self.migrate = migrate + } + #else + init(identifier: String, migrate: @escaping (Database) throws -> Void) { + self.identifier = identifier + self.disabledForeignKeyChecks = false + self.migrate = migrate + } + + @available(iOS 8.2, OSX 10.10, *) + // PRAGMA foreign_key_check was introduced in SQLite 3.7.16 http://www.sqlite.org/changes.html#version_3_7_16 + // It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS) + init(identifier: String, disabledForeignKeyChecks: Bool, migrate: @escaping (Database) throws -> Void) { + self.identifier = identifier + self.disabledForeignKeyChecks = disabledForeignKeyChecks + self.migrate = migrate + } + #endif + + func run(_ db: Database) throws { + if try disabledForeignKeyChecks && (Bool.fetchOne(db, "PRAGMA foreign_keys") ?? false) { + try runWithDisabledForeignKeys(db) + } else { + try runWithoutDisabledForeignKeys(db) + } + } + + + private func runWithoutDisabledForeignKeys(_ db: Database) throws { + try db.inTransaction(.immediate) { + try migrate(db) + try insertAppliedIdentifier(db) + return .commit + } + } + + private func runWithDisabledForeignKeys(_ db: Database) throws { + // Support for database alterations described at + // https://www.sqlite.org/lang_altertable.html#otheralter + // + // > 1. If foreign key constraints are enabled, disable them using + // > PRAGMA foreign_keys=OFF. + try db.execute("PRAGMA foreign_keys = OFF") + + // > 2. Start a transaction. + try db.inTransaction(.immediate) { + try migrate(db) + try insertAppliedIdentifier(db) + + // > 10. If foreign key constraints were originally enabled then run PRAGMA + // > foreign_key_check to verify that the schema change did not break any foreign key + // > constraints. + if try Row.fetchOne(db, "PRAGMA foreign_key_check") != nil { + // https://www.sqlite.org/pragma.html#pragma_foreign_key_check + // + // PRAGMA foreign_key_check does not return an error, + // but the list of violated foreign key constraints. + // + // Let's turn any violation into an SQLITE_CONSTRAINT + // error, and rollback the transaction. + throw DatabaseError(resultCode: .SQLITE_CONSTRAINT, message: "FOREIGN KEY constraint failed") + } + + // > 11. Commit the transaction started in step 2. + return .commit + } + + // > 12. If foreign keys constraints were originally enabled, reenable them now. + try db.execute("PRAGMA foreign_keys = ON") + } + + private func insertAppliedIdentifier(_ db: Database) throws { + try db.execute("INSERT INTO grdb_migrations (identifier) VALUES (?)", arguments: [identifier]) + } +} diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/Column.swift b/Pods/GRDB.swift/GRDB/QueryInterface/Column.swift new file mode 100644 index 0000000..9aa3389 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/Column.swift @@ -0,0 +1,23 @@ +/// A column in the database +/// +/// See https://github.com/groue/GRDB.swift#the-query-interface +public struct Column { + /// The hidden rowID column + public static let rowID = Column("rowid") + + /// The name of the column + public let name: String + + /// Creates a column given its name. + public init(_ name: String) { + self.name = name + } +} + +extension Column : SQLExpression { + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func expressionSQL(_ arguments: inout StatementArguments?) -> String { + return name.quotedDatabaseIdentifier + } +} diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/FTS3+QueryInterfaceRequest.swift b/Pods/GRDB.swift/GRDB/QueryInterface/FTS3+QueryInterfaceRequest.swift new file mode 100644 index 0000000..aa12297 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/FTS3+QueryInterfaceRequest.swift @@ -0,0 +1,56 @@ +extension QueryInterfaceRequest { + + // MARK: Full Text Search + + /// Returns a new QueryInterfaceRequest with a matching predicate added + /// to the eventual set of already applied predicates. + /// + /// // SELECT * FROM books WHERE books MATCH '...' + /// var request = Book.all() + /// request = request.matching(pattern) + /// + /// If the search pattern is nil, the request does not match any + /// database row. + public func matching(_ pattern: FTS3Pattern?) -> QueryInterfaceRequest<T> { + switch query.source { + case .table(let name, let alias)?: + return filter(SQLExpressionBinary(.match, Column(alias ?? name), pattern ?? DatabaseValue.null)) + default: + // Programmer error + fatalError("fts3 match requires a table") + } + } +} + +extension TableMapping { + + // MARK: Full Text Search + + /// Returns a QueryInterfaceRequest with a matching predicate. + /// + /// // SELECT * FROM books WHERE books MATCH '...' + /// var request = Book.matching(pattern) + /// + /// If the search pattern is nil, the request does not match any + /// database row. + /// + /// The selection defaults to all columns. This default can be changed for + /// all requests by the `TableMapping.databaseSelection` property, or + /// for individual requests with the `TableMapping.select` method. + public static func matching(_ pattern: FTS3Pattern?) -> QueryInterfaceRequest<Self> { + return all().matching(pattern) + } +} + +extension Column { + /// A matching SQL expression with the `MATCH` SQL operator. + /// + /// // content MATCH '...' + /// Column("content").match(pattern) + /// + /// If the search pattern is nil, SQLite will evaluate the expression + /// to false. + public func match(_ pattern: FTS3Pattern?) -> SQLExpression { + return SQLExpressionBinary(.match, self, pattern ?? DatabaseValue.null) + } +} diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/FTS5+QueryInterface.swift b/Pods/GRDB.swift/GRDB/QueryInterface/FTS5+QueryInterface.swift new file mode 100644 index 0000000..bb0191e --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/FTS5+QueryInterface.swift @@ -0,0 +1,49 @@ +#if SQLITE_ENABLE_FTS5 + extension QueryInterfaceRequest { + + // MARK: Full Text Search + + /// Returns a new QueryInterfaceRequest with a matching predicate added + /// to the eventual set of already applied predicates. + /// + /// // SELECT * FROM books WHERE books MATCH '...' + /// var request = Book.all() + /// request = request.matching(pattern) + /// + /// If the search pattern is nil, the request does not match any + /// database row. + /// + /// The selection defaults to all columns. This default can be changed for + /// all requests by the `TableMapping.databaseSelection` property, or + /// for individual requests with the `TableMapping.select` method. + public func matching(_ pattern: FTS5Pattern?) -> QueryInterfaceRequest<T> { + switch query.source { + case .table(let name, let alias)?: + if let pattern = pattern { + return filter(SQLExpressionBinary(.match, Column(alias ?? name), pattern)) + } else { + return filter(false) + } + default: + // Programmer error + fatalError("fts5 match requires a table") + } + } + } + + extension TableMapping { + + // MARK: Full Text Search + + /// Returns a QueryInterfaceRequest with a matching predicate. + /// + /// // SELECT * FROM books WHERE books MATCH '...' + /// var request = Book.matching(pattern) + /// + /// If the search pattern is nil, the request does not match any + /// database row. + public static func matching(_ pattern: FTS5Pattern?) -> QueryInterfaceRequest<Self> { + return all().matching(pattern) + } + } +#endif diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/QueryInterfaceRequest.swift b/Pods/GRDB.swift/GRDB/QueryInterface/QueryInterfaceRequest.swift new file mode 100644 index 0000000..e88b99a --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/QueryInterfaceRequest.swift @@ -0,0 +1,373 @@ +/// A QueryInterfaceRequest describes an SQL query. +/// +/// See https://github.com/groue/GRDB.swift#the-query-interface +public struct QueryInterfaceRequest<T> { + let query: QueryInterfaceSelectQueryDefinition + + init(query: QueryInterfaceSelectQueryDefinition) { + self.query = query + } +} + +extension QueryInterfaceRequest : TypedRequest { + public typealias RowDecoder = T + + /// A tuple that contains a prepared statement that is ready to be + /// executed, and an eventual row adapter. + /// + /// - parameter db: A database connection. + public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + return try query.prepare(db) + } + + /// The number of rows fetched by the request. + /// + /// - parameter db: A database connection. + public func fetchCount(_ db: Database) throws -> Int { + return try query.fetchCount(db) + } +} + +extension QueryInterfaceRequest { + + // MARK: Request Derivation + + /// A new QueryInterfaceRequest with a new net of selected columns. + /// + /// // SELECT id, email FROM players + /// var request = Player.all() + /// request = request.select(Column("id"), Column("email")) + /// + /// Any previous selection is replaced: + /// + /// // SELECT email FROM players + /// request + /// .select(Column("id")) + /// .select(Column("email")) + public func select(_ selection: SQLSelectable...) -> QueryInterfaceRequest<T> { + return select(selection) + } + + /// A new QueryInterfaceRequest with a new net of selected columns. + /// + /// // SELECT id, email FROM players + /// var request = Player.all() + /// request = request.select([Column("id"), Column("email")]) + /// + /// Any previous selection is replaced: + /// + /// // SELECT email FROM players + /// request + /// .select([Column("id")]) + /// .select([Column("email")]) + public func select(_ selection: [SQLSelectable]) -> QueryInterfaceRequest<T> { + var query = self.query + query.selection = selection + return QueryInterfaceRequest(query: query) + } + + /// A new QueryInterfaceRequest with a new net of selected columns. + /// + /// // SELECT id, email FROM players + /// var request = Player.all() + /// request = request.select(sql: "id, email") + /// + /// Any previous selection is replaced: + /// + /// // SELECT email FROM players + /// request + /// .select(sql: "id") + /// .select(sql: "email") + public func select(sql: String, arguments: StatementArguments? = nil) -> QueryInterfaceRequest<T> { + return select(SQLExpressionLiteral(sql, arguments: arguments)) + } + + /// A new QueryInterfaceRequest which returns distinct rows. + /// + /// // SELECT DISTINCT * FROM players + /// var request = Player.all() + /// request = request.distinct() + /// + /// // SELECT DISTINCT name FROM players + /// var request = Player.select(Column("name")) + /// request = request.distinct() + public func distinct() -> QueryInterfaceRequest<T> { + var query = self.query + query.isDistinct = true + return QueryInterfaceRequest(query: query) + } + + /// A new QueryInterfaceRequest with the provided *predicate* added to the + /// eventual set of already applied predicates. + /// + /// // SELECT * FROM players WHERE email = 'arthur@example.com' + /// var request = Player.all() + /// request = request.filter(Column("email") == "arthur@example.com") + public func filter(_ predicate: SQLExpressible) -> QueryInterfaceRequest<T> { + var query = self.query + if let whereExpression = query.whereExpression { + query.whereExpression = whereExpression && predicate.sqlExpression + } else { + query.whereExpression = predicate.sqlExpression + } + return QueryInterfaceRequest(query: query) + } + + /// A new QueryInterfaceRequest with the provided *predicate* added to the + /// eventual set of already applied predicates. + /// + /// // SELECT * FROM players WHERE email = 'arthur@example.com' + /// var request = Player.all() + /// request = request.filter(sql: "email = ?", arguments: ["arthur@example.com"]) + public func filter(sql: String, arguments: StatementArguments? = nil) -> QueryInterfaceRequest<T> { + return filter(SQLExpressionLiteral(sql, arguments: arguments)) + } + + /// A new QueryInterfaceRequest grouped according to *expressions*. + public func group(_ expressions: SQLExpressible...) -> QueryInterfaceRequest<T> { + return group(expressions) + } + + /// A new QueryInterfaceRequest grouped according to *expressions*. + public func group(_ expressions: [SQLExpressible]) -> QueryInterfaceRequest<T> { + var query = self.query + query.groupByExpressions = expressions.map { $0.sqlExpression } + return QueryInterfaceRequest(query: query) + } + + /// A new QueryInterfaceRequest with a new grouping. + public func group(sql: String, arguments: StatementArguments? = nil) -> QueryInterfaceRequest<T> { + return group(SQLExpressionLiteral(sql, arguments: arguments)) + } + + /// A new QueryInterfaceRequest with the provided *predicate* added to the + /// eventual set of already applied predicates. + public func having(_ predicate: SQLExpressible) -> QueryInterfaceRequest<T> { + var query = self.query + if let havingExpression = query.havingExpression { + query.havingExpression = (havingExpression && predicate).sqlExpression + } else { + query.havingExpression = predicate.sqlExpression + } + return QueryInterfaceRequest(query: query) + } + + /// A new QueryInterfaceRequest with the provided *sql* added to the + /// eventual set of already applied predicates. + public func having(sql: String, arguments: StatementArguments? = nil) -> QueryInterfaceRequest<T> { + return having(SQLExpressionLiteral(sql, arguments: arguments)) + } + + /// A new QueryInterfaceRequest with the provided *orderings*. + /// + /// // SELECT * FROM players ORDER BY name + /// var request = Player.all() + /// request = request.order(Column("name")) + /// + /// Any previous ordering is replaced: + /// + /// // SELECT * FROM players ORDER BY name + /// request + /// .order(Column("email")) + /// .reversed() + /// .order(Column("name")) + public func order(_ orderings: SQLOrderingTerm...) -> QueryInterfaceRequest<T> { + return order(orderings) + } + + /// A new QueryInterfaceRequest with the provided *orderings*. + /// + /// // SELECT * FROM players ORDER BY name + /// var request = Player.all() + /// request = request.order([Column("name")]) + /// + /// Any previous ordering is replaced: + /// + /// // SELECT * FROM players ORDER BY name + /// request + /// .order([Column("email")]) + /// .reversed() + /// .order([Column("name")]) + public func order(_ orderings: [SQLOrderingTerm]) -> QueryInterfaceRequest<T> { + var query = self.query + query.orderings = orderings + query.isReversed = false + return QueryInterfaceRequest(query: query) + } + + /// A new QueryInterfaceRequest with the provided *sql* used for sorting. + /// + /// // SELECT * FROM players ORDER BY name + /// var request = Player.all() + /// request = request.order(sql: "name") + /// + /// Any previous ordering is replaced: + /// + /// // SELECT * FROM players ORDER BY name + /// request + /// .order(sql: "email") + /// .order(sql: "name") + public func order(sql: String, arguments: StatementArguments? = nil) -> QueryInterfaceRequest<T> { + return order([SQLExpressionLiteral(sql, arguments: arguments)]) + } + + /// A new QueryInterfaceRequest sorted in reversed order. + /// + /// // SELECT * FROM players ORDER BY name DESC + /// var request = Player.all().order(Column("name")) + /// request = request.reversed() + public func reversed() -> QueryInterfaceRequest<T> { + var query = self.query + query.isReversed = !query.isReversed + return QueryInterfaceRequest(query: query) + } + + /// A QueryInterfaceRequest which fetches *limit* rows, starting + /// at *offset*. + /// + /// // SELECT * FROM players LIMIT 1 + /// var request = Player.all() + /// request = request.limit(1) + public func limit(_ limit: Int, offset: Int? = nil) -> QueryInterfaceRequest<T> { + var query = self.query + query.limit = SQLLimit(limit: limit, offset: offset) + return QueryInterfaceRequest(query: query) + } +} + +extension QueryInterfaceRequest where RowDecoder: MutablePersistable { + + // MARK: Deleting + + /// Deletes matching rows; returns the number of deleted rows. + /// + /// - parameter db: A database connection. + /// - returns: The number of deleted rows + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + @discardableResult + public func deleteAll(_ db: Database) throws -> Int { + try query.makeDeleteStatement(db).execute() + return db.changesCount + } +} + +extension TableMapping { + + // MARK: Request Derivation + + /// Creates a QueryInterfaceRequest which fetches all records. + /// + /// // SELECT * FROM players + /// let request = Player.all() + /// + /// The selection defaults to all columns. This default can be changed for + /// all requests by the `TableMapping.databaseSelection` property, or + /// for individual requests with the `TableMapping.select` method. + public static func all() -> QueryInterfaceRequest<Self> { + return QueryInterfaceRequest(query: QueryInterfaceSelectQueryDefinition(select: databaseSelection, from: .table(name: databaseTableName, alias: nil))) + } + + /// Creates a QueryInterfaceRequest which fetches no record. + public static func none() -> QueryInterfaceRequest<Self> { + return filter(false) + } + + /// Creates a QueryInterfaceRequest which selects *selection*. + /// + /// // SELECT id, email FROM players + /// let request = Player.select(Column("id"), Column("email")) + public static func select(_ selection: SQLSelectable...) -> QueryInterfaceRequest<Self> { + return all().select(selection) + } + + /// Creates a QueryInterfaceRequest which selects *selection*. + /// + /// // SELECT id, email FROM players + /// let request = Player.select([Column("id"), Column("email")]) + public static func select(_ selection: [SQLSelectable]) -> QueryInterfaceRequest<Self> { + return all().select(selection) + } + + /// Creates a QueryInterfaceRequest which selects *sql*. + /// + /// // SELECT id, email FROM players + /// let request = Player.select(sql: "id, email") + public static func select(sql: String, arguments: StatementArguments? = nil) -> QueryInterfaceRequest<Self> { + return all().select(sql: sql, arguments: arguments) + } + + /// Creates a QueryInterfaceRequest with the provided *predicate*. + /// + /// // SELECT * FROM players WHERE email = 'arthur@example.com' + /// let request = Player.filter(Column("email") == "arthur@example.com") + /// + /// The selection defaults to all columns. This default can be changed for + /// all requests by the `TableMapping.databaseSelection` property, or + /// for individual requests with the `TableMapping.select` method. + public static func filter(_ predicate: SQLExpressible) -> QueryInterfaceRequest<Self> { + return all().filter(predicate) + } + + /// Creates a QueryInterfaceRequest with the provided *predicate*. + /// + /// // SELECT * FROM players WHERE email = 'arthur@example.com' + /// let request = Player.filter(sql: "email = ?", arguments: ["arthur@example.com"]) + /// + /// The selection defaults to all columns. This default can be changed for + /// all requests by the `TableMapping.databaseSelection` property, or + /// for individual requests with the `TableMapping.select` method. + public static func filter(sql: String, arguments: StatementArguments? = nil) -> QueryInterfaceRequest<Self> { + return all().filter(sql: sql, arguments: arguments) + } + + /// Creates a QueryInterfaceRequest sorted according to the + /// provided *orderings*. + /// + /// // SELECT * FROM players ORDER BY name + /// let request = Player.order(Column("name")) + /// + /// The selection defaults to all columns. This default can be changed for + /// all requests by the `TableMapping.databaseSelection` property, or + /// for individual requests with the `TableMapping.select` method. + public static func order(_ orderings: SQLOrderingTerm...) -> QueryInterfaceRequest<Self> { + return all().order(orderings) + } + + /// Creates a QueryInterfaceRequest sorted according to the + /// provided *orderings*. + /// + /// // SELECT * FROM players ORDER BY name + /// let request = Player.order([Column("name")]) + /// + /// The selection defaults to all columns. This default can be changed for + /// all requests by the `TableMapping.databaseSelection` property, or + /// for individual requests with the `TableMapping.select` method. + public static func order(_ orderings: [SQLOrderingTerm]) -> QueryInterfaceRequest<Self> { + return all().order(orderings) + } + + /// Creates a QueryInterfaceRequest sorted according to *sql*. + /// + /// // SELECT * FROM players ORDER BY name + /// let request = Player.order(sql: "name") + /// + /// The selection defaults to all columns. This default can be changed for + /// all requests by the `TableMapping.databaseSelection` property, or + /// for individual requests with the `TableMapping.select` method. + public static func order(sql: String, arguments: StatementArguments? = nil) -> QueryInterfaceRequest<Self> { + return all().order(sql: sql, arguments: arguments) + } + + /// Creates a QueryInterfaceRequest which fetches *limit* rows, starting at + /// *offset*. + /// + /// // SELECT * FROM players LIMIT 1 + /// let request = Player.limit(1) + /// + /// The selection defaults to all columns. This default can be changed for + /// all requests by the `TableMapping.databaseSelection` property, or + /// for individual requests with the `TableMapping.select` method. + public static func limit(_ limit: Int, offset: Int? = nil) -> QueryInterfaceRequest<Self> { + return all().limit(limit, offset: offset) + } +} diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/QueryInterfaceSelectQueryDefinition.swift b/Pods/GRDB.swift/GRDB/QueryInterface/QueryInterfaceSelectQueryDefinition.swift new file mode 100644 index 0000000..9724f17 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/QueryInterfaceSelectQueryDefinition.swift @@ -0,0 +1,249 @@ +// MARK: - QueryInterfaceSelectQueryDefinition + +struct QueryInterfaceSelectQueryDefinition { + var selection: [SQLSelectable] + var isDistinct: Bool + var source: SQLSource? + var whereExpression: SQLExpression? + var groupByExpressions: [SQLExpression] + var orderings: [SQLOrderingTerm] + var isReversed: Bool + var havingExpression: SQLExpression? + var limit: SQLLimit? + + init( + select selection: [SQLSelectable], + isDistinct: Bool = false, + from source: SQLSource? = nil, + filter whereExpression: SQLExpression? = nil, + groupBy groupByExpressions: [SQLExpression] = [], + orderBy orderings: [SQLOrderingTerm] = [], + isReversed: Bool = false, + having havingExpression: SQLExpression? = nil, + limit: SQLLimit? = nil) + { + self.selection = selection + self.isDistinct = isDistinct + self.source = source + self.whereExpression = whereExpression + self.groupByExpressions = groupByExpressions + self.orderings = orderings + self.isReversed = isReversed + self.havingExpression = havingExpression + self.limit = limit + } + + func sql(_ arguments: inout StatementArguments?) -> String { + var sql = "SELECT" + + if isDistinct { + sql += " DISTINCT" + } + + assert(!selection.isEmpty) + sql += " " + selection.map { $0.resultColumnSQL(&arguments) }.joined(separator: ", ") + + if let source = source { + sql += " FROM " + source.sourceSQL(&arguments) + } + + if let whereExpression = whereExpression { + sql += " WHERE " + whereExpression.expressionSQL(&arguments) + } + + if !groupByExpressions.isEmpty { + sql += " GROUP BY " + groupByExpressions.map { $0.expressionSQL(&arguments) }.joined(separator: ", ") + } + + if let havingExpression = havingExpression { + sql += " HAVING " + havingExpression.expressionSQL(&arguments) + } + + let orderings = self.queryOrderings + if !orderings.isEmpty { + sql += " ORDER BY " + orderings.map { $0.orderingTermSQL(&arguments) }.joined(separator: ", ") + } + + if let limit = limit { + sql += " LIMIT " + limit.sql + } + + return sql + } + + func makeDeleteStatement(_ db: Database) throws -> UpdateStatement { + guard groupByExpressions.isEmpty else { + // Programmer error + fatalError("Can't delete query with GROUP BY expression") + } + + guard havingExpression == nil else { + // Programmer error + fatalError("Can't delete query with GROUP BY expression") + } + + var sql = "DELETE" + var arguments: StatementArguments? = StatementArguments() + + if let source = source { + sql += " FROM " + source.sourceSQL(&arguments) + } + + if let whereExpression = whereExpression { + sql += " WHERE " + whereExpression.expressionSQL(&arguments) + } + + if let limit = limit { + let orderings = self.queryOrderings + if !orderings.isEmpty { + sql += " ORDER BY " + orderings.map { $0.orderingTermSQL(&arguments) }.joined(separator: ", ") + } + + if Database.sqliteCompileOptions.contains("ENABLE_UPDATE_DELETE_LIMIT") { + sql += " LIMIT " + limit.sql + } else { + fatalError("Can't delete query with limit") + } + } + + let statement = try db.makeUpdateStatement(sql) + statement.arguments = arguments! + return statement + } + + private var queryOrderings: [SQLOrderingTerm] { + if isReversed { + if orderings.isEmpty { + // https://www.sqlite.org/lang_createtable.html#rowid + // + // > The rowid value can be accessed using one of the special + // > case-independent names "rowid", "oid", or "_rowid_" in + // > place of a column name. If a table contains a user defined + // > column named "rowid", "oid" or "_rowid_", then that name + // > always refers the explicitly declared column and cannot be + // > used to retrieve the integer rowid value. + // + // Here we assume that rowid is not a custom column. + // TODO: support for user-defined rowid column. + // TODO: support for WITHOUT ROWID tables. + return [Column.rowID.desc] + } else { + return orderings.map { $0.reversed } + } + } else { + return orderings + } + } + + /// Remove ordering + var unorderedQuery: QueryInterfaceSelectQueryDefinition { + var query = self + query.isReversed = false + query.orderings = [] + return query + } +} + +extension QueryInterfaceSelectQueryDefinition : Request { + func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { + var arguments: StatementArguments? = StatementArguments() + let sql = self.sql(&arguments) + let statement = try db.makeSelectStatement(sql) + try statement.setArgumentsWithValidation(arguments!) + return (statement, nil) + } + + func fetchCount(_ db: Database) throws -> Int { + return try Int.fetchOne(db, countQuery)! + } + + private var countQuery: QueryInterfaceSelectQueryDefinition { + guard groupByExpressions.isEmpty && limit == nil else { + // SELECT ... GROUP BY ... + // SELECT ... LIMIT ... + return trivialCountQuery + } + + guard let source = source, case .table = source else { + // SELECT ... FROM (something which is not a table) + return trivialCountQuery + } + + assert(!selection.isEmpty) + if selection.count == 1 { + guard let count = self.selection[0].count(distinct: isDistinct) else { + return trivialCountQuery + } + var countQuery = unorderedQuery + countQuery.isDistinct = false + countQuery.selection = [count.sqlSelectable] + return countQuery + } else { + // SELECT [DISTINCT] expr1, expr2, ... FROM tableName ... + + guard !isDistinct else { + return trivialCountQuery + } + + // SELECT expr1, expr2, ... FROM tableName ... + // -> + // SELECT COUNT(*) FROM tableName ... + var countQuery = unorderedQuery + countQuery.selection = [SQLExpressionCount(AllColumns())] + return countQuery + } + } + + // SELECT COUNT(*) FROM (self) + private var trivialCountQuery: QueryInterfaceSelectQueryDefinition { + return QueryInterfaceSelectQueryDefinition( + select: [SQLExpressionCount(AllColumns())], + from: .query(query: unorderedQuery, alias: nil)) + } +} + +indirect enum SQLSource { + case table(name: String, alias: String?) + case query(query: QueryInterfaceSelectQueryDefinition, alias: String?) + + func sourceSQL(_ arguments: inout StatementArguments?) -> String { + switch self { + case .table(let table, let alias): + if let alias = alias { + return table.quotedDatabaseIdentifier + " AS " + alias.quotedDatabaseIdentifier + } else { + return table.quotedDatabaseIdentifier + } + case .query(let query, let alias): + if let alias = alias { + return "(" + query.sql(&arguments) + ") AS " + alias.quotedDatabaseIdentifier + } else { + return "(" + query.sql(&arguments) + ")" + } + } + } +} + +struct SQLLimit { + let limit: Int + let offset: Int? + + var sql: String { + if let offset = offset { + return "\(limit) OFFSET \(offset)" + } else { + return "\(limit)" + } + } +} + +extension SQLCount { + var sqlSelectable: SQLSelectable { + switch self { + case .all: + return SQLExpressionCount(AllColumns()) + case .distinct(let expression): + return SQLExpressionCountDistinct(expression) + } + } +} diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/SQLCollatedExpression.swift b/Pods/GRDB.swift/GRDB/QueryInterface/SQLCollatedExpression.swift new file mode 100644 index 0000000..e8c9d34 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/SQLCollatedExpression.swift @@ -0,0 +1,62 @@ +/// SQLCollatedExpression taints an expression so that every derived expression +/// is eventually evaluated using an SQLite collation. +/// +/// You create one by calling the SQLSpecificExpressible.collating() method. +/// +/// let email: SQLCollatedExpression = Column("email").collating(.nocase) +/// +/// // SELECT * FROM players WHERE email = 'arthur@example.com' COLLATE NOCASE +/// Players.filter(email == "arthur@example.com") +public struct SQLCollatedExpression { + /// The tainted expression + public let expression: SQLExpression + + /// The name of the collation + public let collationName: Database.CollationName + + /// Returns an ordering suitable for QueryInterfaceRequest.order() + /// + /// let email: SQLCollatedExpression = Column("email").collating(.nocase) + /// + /// // SELECT * FROM players ORDER BY email COLLATE NOCASE ASC + /// Players.order(email.asc) + /// + /// See https://github.com/groue/GRDB.swift/#the-query-interface + public var asc: SQLOrderingTerm { + return SQLOrdering.asc(sqlExpression) + } + + /// Returns an ordering suitable for QueryInterfaceRequest.order() + /// + /// let email: SQLCollatedExpression = Column("email").collating(.nocase) + /// + /// // SELECT * FROM players ORDER BY email COLLATE NOCASE DESC + /// Players.order(email.desc) + /// + /// See https://github.com/groue/GRDB.swift/#the-query-interface + public var desc: SQLOrderingTerm { + return SQLOrdering.desc(sqlExpression) + } + + init(_ expression: SQLExpression, collationName: Database.CollationName) { + self.expression = expression + self.collationName = collationName + } + + var sqlExpression: SQLExpression { + return SQLExpressionCollate(expression, collationName: collationName) + } +} + +extension SQLCollatedExpression : SQLOrderingTerm { + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public var reversed: SQLOrderingTerm { + return desc + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func orderingTermSQL(_ arguments: inout StatementArguments?) -> String { + return sqlExpression.orderingTermSQL(&arguments) + } +} diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/SQLCollection.swift b/Pods/GRDB.swift/GRDB/QueryInterface/SQLCollection.swift new file mode 100644 index 0000000..3b58279 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/SQLCollection.swift @@ -0,0 +1,70 @@ +// MARK: - SQLCollection + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// SQLCollection is the protocol for types that can be checked for inclusion. +public protocol SQLCollection { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Returns an SQL string that represents the collection. + /// + /// When the arguments parameter is nil, any value must be written down as + /// a literal in the returned SQL: + /// + /// var arguments: StatementArguments? = nil + /// let collection = SQLExpressionsArray([1,2,3]) + /// collection.collectionSQL(&arguments) // "1,2,3" + /// + /// When the arguments parameter is not nil, then values may be replaced by + /// `?` or colon-prefixed tokens, and fed into arguments. + /// + /// var arguments = StatementArguments() + /// let collection = SQLExpressionsArray([1,2,3]) + /// collection.collectionSQL(&arguments) // "?,?,?" + /// arguments // [1,2,3] + func collectionSQL(_ arguments: inout StatementArguments?) -> String + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Returns an expression that check whether the collection contains + /// the expression. + func contains(_ value: SQLExpressible) -> SQLExpression +} + + +// MARK: Default Implementations + +extension SQLCollection { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Returns a SQLExpressionContains which applies the `IN` SQL operator. + public func contains(_ value: SQLExpressible) -> SQLExpression { + return SQLExpressionContains(value, self) + } +} + + +// MARK: - SQLExpressionsArray + +/// SQLExpressionsArray wraps an array of expressions +/// +/// SQLExpressionsArray([1, 2, 3]) +struct SQLExpressionsArray : SQLCollection { + let expressions: [SQLExpression] + + init<S: Sequence>(_ expressions: S) where S.Iterator.Element : SQLExpressible { + self.expressions = expressions.map { $0.sqlExpression } + } + + func collectionSQL(_ arguments: inout StatementArguments?) -> String { + return (expressions.map { $0.expressionSQL(&arguments) } as [String]).joined(separator: ", ") + } + + func contains(_ value: SQLExpressible) -> SQLExpression { + if expressions.isEmpty { + return false.databaseValue + } else { + return SQLExpressionContains(value, self) + } + } +} diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/SQLExpressible.swift b/Pods/GRDB.swift/GRDB/QueryInterface/SQLExpressible.swift new file mode 100644 index 0000000..e4906d3 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/SQLExpressible.swift @@ -0,0 +1,84 @@ +// MARK: - SQLExpressible + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// The protocol for all types that can be turned into an SQL expression. +/// +/// It is adopted by protocols like DatabaseValueConvertible, and types +/// like Column. +/// +/// See https://github.com/groue/GRDB.swift/#the-query-interface +public protocol SQLExpressible { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Returns an SQLExpression + var sqlExpression: SQLExpression { get } +} + +// MARK: - SQLSpecificExpressible + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// SQLSpecificExpressible is a protocol for all database-specific types that can +/// be turned into an SQL expression. Types whose existence is not purely +/// dedicated to the database should adopt the SQLExpressible protocol instead. +/// +/// For example, Column is a type that only exists to help you build requests, +/// and it adopts SQLSpecificExpressible. +/// +/// On the other side, Int adopts SQLExpressible (via DatabaseValueConvertible). +public protocol SQLSpecificExpressible : SQLExpressible { + // SQLExpressible can be adopted by Swift standard types, and user + // types, through the DatabaseValueConvertible protocol which inherits + // from SQLExpressible. + // + // For example, Int adopts SQLExpressible through + // DatabaseValueConvertible. + // + // SQLSpecificExpressible, on the other side, is not adopted by any + // Swift standard type or any user type. It is only adopted by GRDB types, + // such as Column and SQLExpression. + // + // This separation lets us define functions and operators that do not + // spill out. The three declarations below have no chance overloading a + // Swift-defined operator, or a user-defined operator: + // + // - ==(SQLExpressible, SQLSpecificExpressible) + // - ==(SQLSpecificExpressible, SQLExpressible) + // - ==(SQLSpecificExpressible, SQLSpecificExpressible) +} + +// MARK: - SQLExpressible & SQLOrderingTerm + +extension SQLExpressible where Self: SQLOrderingTerm { + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public var reversed: SQLOrderingTerm { + return SQLOrdering.desc(sqlExpression) + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func orderingTermSQL(_ arguments: inout StatementArguments?) -> String { + return sqlExpression.expressionSQL(&arguments) + } +} + +// MARK: - SQLExpressible & SQLSelectable + +extension SQLExpressible where Self: SQLSelectable { + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func resultColumnSQL(_ arguments: inout StatementArguments?) -> String { + return sqlExpression.expressionSQL(&arguments) + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func countedSQL(_ arguments: inout StatementArguments?) -> String { + return sqlExpression.expressionSQL(&arguments) + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func count(distinct: Bool) -> SQLCount? { + return sqlExpression.count(distinct: distinct) + } +} diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/SQLExpression+QueryInterface.swift b/Pods/GRDB.swift/GRDB/QueryInterface/SQLExpression+QueryInterface.swift new file mode 100644 index 0000000..266a919 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/SQLExpression+QueryInterface.swift @@ -0,0 +1,472 @@ +// MARK: - SQLExpression + +extension SQLExpression { + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Converts an expression to an SQLExpressionLiteral + public var literal: SQLExpressionLiteral { + var arguments: StatementArguments? = [] + let sql = expressionSQL(&arguments) + return SQLExpressionLiteral(sql, arguments: arguments) + } + + /// The expression as a quoted SQL literal (not public in order to avoid abuses) + /// + /// "foo'bar".databaseValue.sql // "'foo''bar'"" + var sql: String { + var arguments: StatementArguments? = nil + return expressionSQL(&arguments) + } +} + +// MARK: - SQLExpressionLiteral + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// SQLExpressionLiteral is an expression built from a raw SQL snippet. +/// +/// SQLExpressionLiteral("1 + 2") +/// +/// The SQL literal may contain `?` and colon-prefixed tokens: +/// +/// SQLExpressionLiteral("? + ?", arguments: [1, 2]) +/// SQLExpressionLiteral(":one + :two", arguments: ["one": 1, "two": 2]) +public struct SQLExpressionLiteral : SQLExpression { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// The SQL literal + public let sql: String + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Eventual arguments that feed the `?` and colon-prefixed tokens in the + /// SQL literal + public let arguments: StatementArguments? + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Creates an SQL literal expression. + /// + /// SQLExpressionLiteral("1 + 2") + /// SQLExpressionLiteral("? + ?", arguments: [1, 2]) + /// SQLExpressionLiteral(":one + :two", arguments: ["one": 1, "two": 2]) + public init(_ sql: String, arguments: StatementArguments? = nil) { + self.sql = sql + self.arguments = arguments + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func expressionSQL(_ arguments: inout StatementArguments?) -> String { + if let literalArguments = self.arguments { + guard arguments != nil else { + // GRDB limitation: we don't know how to look for `?` in sql and + // replace them with with literals. + fatalError("Not implemented") + } + arguments! += literalArguments + } + return sql + } +} + +// MARK: - SQLExpressionUnary + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// SQLUnaryOperator is a SQLite unary operator. +public struct SQLUnaryOperator : Hashable { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// The SQL operator + public let sql: String + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// If true GRDB puts a white space between the operator and the operand. + public let needsRightSpace: Bool + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Creates an unary operator + /// + /// SQLUnaryOperator("~", needsRightSpace: false) + public init(_ sql: String, needsRightSpace: Bool) { + self.sql = sql + self.needsRightSpace = needsRightSpace + } + + /// The hash value + public var hashValue: Int { + return sql.hashValue + } + + /// Equality operator + public static func == (lhs: SQLUnaryOperator, rhs: SQLUnaryOperator) -> Bool { + return lhs.sql == rhs.sql + } +} + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// SQLExpressionUnary is an expression made of an unary operator and +/// an operand expression. +/// +/// SQLExpressionUnary(.not, Column("favorite")) +public struct SQLExpressionUnary : SQLExpression { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// The unary operator + public let op: SQLUnaryOperator + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// The operand + public let expression: SQLExpression + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Creates an expression made of an unary operator and + /// an operand expression. + /// + /// // NOT favorite + /// SQLExpressionUnary(.not, Column("favorite")) + public init(_ op: SQLUnaryOperator, _ value: SQLExpressible) { + self.op = op + self.expression = value.sqlExpression + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func expressionSQL(_ arguments: inout StatementArguments?) -> String { + return op.sql + (op.needsRightSpace ? " " : "") + expression.expressionSQL(&arguments) + } +} + +// MARK: - SQLExpressionBinary + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// SQLBinaryOperator is a SQLite binary operator. +public struct SQLBinaryOperator : Hashable { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// The SQL operator + public let sql: String + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// The SQL for the negated operator, if any + public let negatedSQL: String? + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Creates a binary operator + /// + /// SQLBinaryOperator("+") + /// SQLBinaryOperator("IS", negated: "IS NOT") + public init(_ sql: String, negated: String? = nil) { + self.sql = sql + self.negatedSQL = negated + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Returns the negated binary operator, if any + /// + /// let operator = SQLBinaryOperator("IS", negated: "IS NOT") + /// operator.negated!.sql // IS NOT + public var negated: SQLBinaryOperator? { + guard let negatedSQL = negatedSQL else { + return nil + } + return SQLBinaryOperator(negatedSQL, negated: sql) + } + + /// The hash value + public var hashValue: Int { + return sql.hashValue + } + + /// Equality operator + public static func == (lhs: SQLBinaryOperator, rhs: SQLBinaryOperator) -> Bool { + return lhs.sql == rhs.sql + } + + // TODO: make it an extension of Sequence (like joined(separator:)) when Swift can better handle existentials + // TODO: make it public eventually + /// Return nil if expressions is empty. + func join(_ expressions: [SQLExpression]) -> SQLExpression? { + switch expressions.count { + case 0: + return nil + case 1: + return expressions[0] + default: + let literals = expressions.map { $0.literal } + let firstLiteral = literals[0] + var sql = firstLiteral.sql + var arguments = firstLiteral.arguments ?? StatementArguments() + for literal in literals.suffix(from: 1) { + sql += " \(self.sql) \(literal.sql)" + if let args = literal.arguments { + arguments += args + } + } + return SQLExpressionLiteral("(\(sql))", arguments: arguments.isEmpty ? nil : arguments) + } + } +} + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// SQLExpressionBinary is an expression made of two expressions joined with a +/// binary operator. +/// +/// SQLExpressionBinary(.multiply, Column("length"), Column("width")) +public struct SQLExpressionBinary : SQLExpression { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// The left operand + public let lhs: SQLExpression + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// The operator + public let op: SQLBinaryOperator + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// The right operand + public let rhs: SQLExpression + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Creates an expression made of two expressions joined with a + /// binary operator. + /// + /// // length * width + /// SQLExpressionBinary(.multiply, Column("length"), Column("width")) + public init(_ op: SQLBinaryOperator, _ lhs: SQLExpressible, _ rhs: SQLExpressible) { + self.lhs = lhs.sqlExpression + self.op = op + self.rhs = rhs.sqlExpression + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func expressionSQL(_ arguments: inout StatementArguments?) -> String { + return "(" + lhs.expressionSQL(&arguments) + " " + op.sql + " " + rhs.expressionSQL(&arguments) + ")" + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public var negated: SQLExpression { + if let negatedOp = op.negated { + return SQLExpressionBinary(negatedOp, lhs, rhs) + } else { + return SQLExpressionNot(self) + } + } +} + +// MARK: - SQLExpressionContains + +/// SQLExpressionContains is an expression that checks the inclusion of a +/// value in a collection with the `IN` operator. +/// +/// // id IN (1,2,3) +/// SQLExpressionContains(Column("id"), SQLExpressionsArray([1,2,3])) +struct SQLExpressionContains : SQLExpression { + let expression: SQLExpression + let collection: SQLCollection + let isNegated: Bool + + init(_ value: SQLExpressible, _ collection: SQLCollection, negated: Bool = false) { + self.expression = value.sqlExpression + self.collection = collection + self.isNegated = negated + } + + func expressionSQL(_ arguments: inout StatementArguments?) -> String { + return "(" + + expression.expressionSQL(&arguments) + + (isNegated ? " NOT IN (" : " IN (") + + collection.collectionSQL(&arguments) + + "))" + } + + var negated: SQLExpression { + return SQLExpressionContains(expression, collection, negated: !isNegated) + } +} + +// MARK: - SQLExpressionBetween + +/// SQLExpressionBetween is an expression that checks if a values is included +/// in a range with the `BETWEEN` operator. +/// +/// // id BETWEEN 1 AND 3 +/// SQLExpressionBetween(Column("id"), 1.databaseValue, 3.databaseValue) +struct SQLExpressionBetween : SQLExpression { + let expression: SQLExpression + let lowerBound: SQLExpression + let upperBound: SQLExpression + let isNegated: Bool + + init(_ expression: SQLExpression, _ lowerBound: SQLExpression, _ upperBound: SQLExpression, negated: Bool = false) { + self.expression = expression + self.lowerBound = lowerBound + self.upperBound = upperBound + self.isNegated = negated + } + + func expressionSQL(_ arguments: inout StatementArguments?) -> String { + return "(" + + expression.expressionSQL(&arguments) + + (isNegated ? " NOT BETWEEN " : " BETWEEN ") + + lowerBound.expressionSQL(&arguments) + + " AND " + + upperBound.expressionSQL(&arguments) + + ")" + } + + var negated: SQLExpression { + return SQLExpressionBetween(expression, lowerBound, upperBound, negated: !isNegated) + } +} + +// MARK: - SQLExpressionFunction + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// SQLFunctionName is an SQL function name. +public struct SQLFunctionName : Hashable { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// The SQL function name + public let sql: String + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Creates a function name + /// + /// SQLFunctionName("ABS") + public init(_ sql: String) { + self.sql = sql + } + + /// The hash value + public var hashValue: Int { + return sql.hashValue + } + + /// Equality operator + public static func == (lhs: SQLFunctionName, rhs: SQLFunctionName) -> Bool { + return lhs.sql == rhs.sql + } +} + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// SQLExpressionFunction is an SQL function call. +/// +/// // ABS(-1) +/// SQLExpressionFunction(.abs, [-1.databaseValue]) +public struct SQLExpressionFunction : SQLExpression { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// The function name + public let functionName: SQLFunctionName + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// The function arguments + public let arguments: [SQLExpression] + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Creates an SQL function call + /// + /// // ABS(-1) + /// SQLExpressionFunction(.abs, arguments: [-1.databaseValue]) + public init(_ functionName: SQLFunctionName, arguments: [SQLExpression]) { + self.functionName = functionName + self.arguments = arguments + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Creates an SQL function call + /// + /// // ABS(-1) + /// SQLExpressionFunction(.abs, arguments: -1) + public init(_ functionName: SQLFunctionName, arguments: SQLExpressible...) { + self.init(functionName, arguments: arguments.map { $0.sqlExpression }) + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func expressionSQL(_ arguments: inout StatementArguments?) -> String { + return functionName.sql + "(" + (self.arguments.map { $0.expressionSQL(&arguments) } as [String]).joined(separator: ", ") + ")" + } +} + +// MARK: - SQLExpressionCount + +/// SQLExpressionCount is a call to the SQL `COUNT` function. +/// +/// // COUNT(name) +/// SQLExpressionCount(Column("name")) +struct SQLExpressionCount : SQLExpression { + /// The counted value + let counted: SQLSelectable + + init(_ counted: SQLSelectable) { + self.counted = counted + } + + func expressionSQL(_ arguments: inout StatementArguments?) -> String { + return "COUNT(" + counted.countedSQL(&arguments) + ")" + } +} + +// MARK: - SQLExpressionCountDistinct + +/// SQLExpressionCountDistinct is a call to the SQL `COUNT(DISTINCT ...)` function. +/// +/// // COUNT(DISTINCT name) +/// SQLExpressionCountDistinct(Column("name")) +struct SQLExpressionCountDistinct : SQLExpression { + let counted: SQLExpression + + init(_ counted: SQLExpression) { + self.counted = counted + } + + func expressionSQL(_ arguments: inout StatementArguments?) -> String { + return "COUNT(DISTINCT " + counted.expressionSQL(&arguments) + ")" + } +} + +// MARK: - SQLExpressionCollate + +/// SQLExpressionCollate is an expression tainted by an SQLite collation. +/// +/// // email = 'arthur@example.com' COLLATE NOCASE +/// SQLExpressionCollate(Column("email") == "arthur@example.com", "NOCASE") +struct SQLExpressionCollate : SQLExpression { + let expression: SQLExpression + let collationName: Database.CollationName + + init(_ expression: SQLExpression, collationName: Database.CollationName) { + self.expression = expression + self.collationName = collationName + } + + func expressionSQL(_ arguments: inout StatementArguments?) -> String { + let sql = expression.expressionSQL(&arguments) + let chars = sql.characters + if chars.last! == ")" { + return String(chars.prefix(upTo: chars.index(chars.endIndex, offsetBy: -1))) + " COLLATE " + collationName.rawValue + ")" + } else { + return sql + " COLLATE " + collationName.rawValue + } + } +} diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/SQLExpression.swift b/Pods/GRDB.swift/GRDB/QueryInterface/SQLExpression.swift new file mode 100644 index 0000000..a9d1328 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/SQLExpression.swift @@ -0,0 +1,118 @@ +// MARK: - SQLExpression + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// SQLExpression is the protocol for types that represent an SQL expression, as +/// described at https://www.sqlite.org/lang_expr.html +/// +/// GRDB ships with a variety of types that already adopt this protocol, and +/// allow to represent many SQLite expressions: +/// +/// - Column +/// - DatabaseValue +/// - SQLExpressionLiteral +/// - SQLExpressionUnary +/// - SQLExpressionBinary +/// - SQLExpressionExists +/// - SQLExpressionFunction +/// - SQLExpressionCollate +public protocol SQLExpression : SQLSpecificExpressible, SQLSelectable, SQLOrderingTerm { + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Returns an SQL string that represents the expression. + /// + /// When the arguments parameter is nil, any value must be written down as + /// a literal in the returned SQL: + /// + /// var arguments: StatementArguments? = nil + /// let expression = "foo'bar".databaseValue + /// expression.expressionSQL(&arguments) // "'foo''bar'" + /// + /// When the arguments parameter is not nil, then values may be replaced by + /// `?` or colon-prefixed tokens, and fed into arguments. + /// + /// var arguments = StatementArguments() + /// let expression = "foo'bar".databaseValue + /// expression.expressionSQL(&arguments) // "?" + /// arguments // ["foo'bar"] + func expressionSQL(_ arguments: inout StatementArguments?) -> String + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Returns the expression, negated. This property fuels the `!` operator. + /// + /// The default implementation returns the expression prefixed by `NOT`. + /// + /// let column = Column("favorite") + /// column.negated // NOT favorite + /// + /// Some expressions may provide a custom implementation that returns a + /// more natural SQL expression. + /// + /// let expression = [1,2,3].contains(Column("id")) // id IN (1,2,3) + /// expression.negated // id NOT IN (1,2,3) + var negated: SQLExpression { get } +} + +extension SQLExpression { + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// The default implementation returns the expression prefixed by `NOT`. + /// + /// let column = Column("favorite") + /// column.negated // NOT favorite + /// + public var negated: SQLExpression { + return SQLExpressionNot(self) + } +} + +// SQLExpression: SQLExpressible + +extension SQLExpression { + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public var sqlExpression: SQLExpression { + return self + } +} + +// SQLExpression: SQLSelectable + +extension SQLExpression { + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func count(distinct: Bool) -> SQLCount? { + if distinct { + // SELECT DISTINCT expr FROM tableName ... + // -> + // SELECT COUNT(DISTINCT expr) FROM tableName ... + return .distinct(self) + } else { + // SELECT expr FROM tableName ... + // -> + // SELECT COUNT(*) FROM tableName ... + return .all + } + } +} + +// MARK: - SQLExpressionNot + +struct SQLExpressionNot : SQLExpression { + let expression: SQLExpression + + init(_ expression: SQLExpression) { + self.expression = expression + } + + func expressionSQL(_ arguments: inout StatementArguments?) -> String { + return "NOT \(expression.expressionSQL(&arguments))" + } + + var negated: SQLExpression { + return expression + } +} diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/SQLOrdering.swift b/Pods/GRDB.swift/GRDB/QueryInterface/SQLOrdering.swift new file mode 100644 index 0000000..2fc88ad --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/SQLOrdering.swift @@ -0,0 +1,57 @@ +// MARK: - SQLOrderingTerm + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// The protocol for all types that can be used as an SQL ordering term, as +/// described at https://www.sqlite.org/syntax/ordering-term.html +public protocol SQLOrderingTerm { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// The ordering term, reversed + var reversed: SQLOrderingTerm { get } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Returns an SQL string that represents the ordering term. + /// + /// When the arguments parameter is nil, any value must be written down as + /// a literal in the returned SQL: + /// + /// var arguments: StatementArguments? = nil + /// let orderingTerm = Column("name") ?? "Anonymous" + /// orderingTerm.orderingTermSQL(&arguments) // "IFNULL(name, 'Anonymous')" + /// + /// When the arguments parameter is not nil, then values may be replaced by + /// `?` or colon-prefixed tokens, and fed into arguments. + /// + /// var arguments = StatementArguments() + /// let orderingTerm = Column("name") ?? "Anonymous" + /// orderingTerm.orderingTermSQL(&arguments) // "IFNULL(name, ?)" + /// arguments // ["Anonymous"] + func orderingTermSQL(_ arguments: inout StatementArguments?) -> String +} + +// MARK: - SQLOrdering + +enum SQLOrdering : SQLOrderingTerm { + case asc(SQLExpression) + case desc(SQLExpression) + + var reversed: SQLOrderingTerm { + switch self { + case .asc(let expression): + return SQLOrdering.desc(expression) + case .desc(let expression): + return SQLOrdering.asc(expression) + } + } + + func orderingTermSQL(_ arguments: inout StatementArguments?) -> String { + switch self { + case .asc(let expression): + return expression.expressionSQL(&arguments) + " ASC" + case .desc(let expression): + return expression.expressionSQL(&arguments) + " DESC" + } + } +} diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/SQLSelectable+QueryInterface.swift b/Pods/GRDB.swift/GRDB/QueryInterface/SQLSelectable+QueryInterface.swift new file mode 100644 index 0000000..9634bba --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/SQLSelectable+QueryInterface.swift @@ -0,0 +1,70 @@ +// MARK: - AllColumns + +/// AllColumns is the `*` in `SELECT *`. +/// +/// You use AllColumns in your custom implementation of +/// TableMapping.databaseSelection. +/// +/// For example: +/// +/// struct Player : TableMapping { +/// static var databaseTableName = "players" +/// static let databaseSelection: [SQLSelectable] = [AllColumns(), Column.rowID] +/// } +/// +/// // SELECT *, rowid FROM players +/// let request = Player.all() +public struct AllColumns { + /// + public init() { } +} + +extension AllColumns : SQLSelectable { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func resultColumnSQL(_ arguments: inout StatementArguments?) -> String { + return "*" + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func countedSQL(_ arguments: inout StatementArguments?) -> String { + return "*" + } + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public func count(distinct: Bool) -> SQLCount? { + // SELECT DISTINCT * FROM tableName ... + guard !distinct else { + return nil + } + + // SELECT * FROM tableName ... + // -> + // SELECT COUNT(*) FROM tableName ... + return .all + } +} + + +// MARK: - SQLAliasedExpression + +struct SQLAliasedExpression : SQLSelectable { + let expression: SQLExpression + let alias: String + + init(_ expression: SQLExpression, alias: String) { + self.expression = expression + self.alias = alias + } + + func resultColumnSQL(_ arguments: inout StatementArguments?) -> String { + return expression.resultColumnSQL(&arguments) + " AS " + alias.quotedDatabaseIdentifier + } + + func countedSQL(_ arguments: inout StatementArguments?) -> String { + return expression.countedSQL(&arguments) + } + + func count(distinct: Bool) -> SQLCount? { + return expression.count(distinct: distinct) + } +} diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/SQLSelectable.swift b/Pods/GRDB.swift/GRDB/QueryInterface/SQLSelectable.swift new file mode 100644 index 0000000..c349298 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/SQLSelectable.swift @@ -0,0 +1,31 @@ +// MARK: - SQLSelectable + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// SQLSelectable is the protocol for types that can be selected, as +/// described at https://www.sqlite.org/syntax/result-column.html +public protocol SQLSelectable { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + func resultColumnSQL(_ arguments: inout StatementArguments?) -> String + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + func countedSQL(_ arguments: inout StatementArguments?) -> String + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + func count(distinct: Bool) -> SQLCount? +} + +// MARK: - Counting + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +public enum SQLCount { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Represents COUNT(*) + case all + + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + /// + /// Represents COUNT(DISTINCT expression) + case distinct(SQLExpression) +} diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/SQLSpecificExpressible+QueryInterface.swift b/Pods/GRDB.swift/GRDB/QueryInterface/SQLSpecificExpressible+QueryInterface.swift new file mode 100644 index 0000000..ffe8c17 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/SQLSpecificExpressible+QueryInterface.swift @@ -0,0 +1,56 @@ +// MARK: - SQL Ordering Support + +extension SQLSpecificExpressible { + + /// Returns a value that can be used as an argument to QueryInterfaceRequest.order() + /// + /// See https://github.com/groue/GRDB.swift/#the-query-interface + public var asc: SQLOrderingTerm { + return SQLOrdering.asc(sqlExpression) + } + + /// Returns a value that can be used as an argument to QueryInterfaceRequest.order() + /// + /// See https://github.com/groue/GRDB.swift/#the-query-interface + public var desc: SQLOrderingTerm { + return SQLOrdering.desc(sqlExpression) + } +} + + +// MARK: - SQL Selection Support + +extension SQLSpecificExpressible { + + /// Returns a value that can be used as an argument to QueryInterfaceRequest.select() + /// + /// See https://github.com/groue/GRDB.swift/#the-query-interface + public func aliased(_ alias: String) -> SQLSelectable { + return SQLAliasedExpression(sqlExpression, alias: alias) + } +} + + +// MARK: - SQL Collations Support + +extension SQLSpecificExpressible { + + /// Returns a collated expression. + /// + /// For example: + /// + /// Player.filter(Column("email").collating(.nocase) == "contact@example.com") + public func collating(_ collation: Database.CollationName) -> SQLCollatedExpression { + return SQLCollatedExpression(sqlExpression, collationName: collation) + } + + /// Returns a collated expression. + /// + /// For example: + /// + /// Player.filter(Column("name").collating(.localizedStandardCompare) == "Hervé") + public func collating(_ collation: DatabaseCollation) -> SQLCollatedExpression { + return SQLCollatedExpression(sqlExpression, collationName: Database.CollationName(collation.name)) + } +} + diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/Support/SQLFunctions.swift b/Pods/GRDB.swift/GRDB/QueryInterface/Support/SQLFunctions.swift new file mode 100644 index 0000000..1f6e410 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/Support/SQLFunctions.swift @@ -0,0 +1,214 @@ +// MARK: - Custom Functions + +extension DatabaseFunction { + /// Returns an SQL expression that applies the function. + /// + /// See https://github.com/groue/GRDB.swift/#sql-functions + public func apply(_ arguments: SQLExpressible...) -> SQLExpression { + return SQLExpressionFunction(SQLFunctionName(name), arguments: arguments.map { $0.sqlExpression }) + } +} + + +// MARK: - ABS(...) + +extension SQLFunctionName { + /// The `ABS` function name + public static let abs = SQLFunctionName("ABS") +} + +/// Returns an expression that evaluates the `ABS` SQL function. +/// +/// // ABS(amount) +/// abs(Column("amount")) +public func abs(_ value: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionFunction(.abs, arguments: value) +} + + +// MARK: - AVG(...) + +extension SQLFunctionName { + /// The `AVG` function name + public static let avg = SQLFunctionName("AVG") +} + +/// Returns an expression that evaluates the `AVG` SQL function. +/// +/// // AVG(length) +/// average(Column("length")) +public func average(_ value: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionFunction(.avg, arguments: value) +} + + +// MARK: - COUNT(...) + +/// Returns an expression that evaluates the `COUNT` SQL function. +/// +/// // COUNT(email) +/// count(Column("email")) +public func count(_ counted: SQLSelectable) -> SQLExpression { + return SQLExpressionCount(counted) +} + + +// MARK: - COUNT(DISTINCT ...) + +/// Returns an expression that evaluates the `COUNT(DISTINCT)` SQL function. +/// +/// // COUNT(DISTINCT email) +/// count(distinct: Column("email")) +public func count(distinct value: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionCountDistinct(value.sqlExpression) +} + + +// MARK: - IFNULL(...) + +extension SQLFunctionName { + /// The `IFNULL` function name + public static let ifNull = SQLFunctionName("IFNULL") +} + +/// Returns an expression that evaluates the `IFNULL` SQL function. +/// +/// // IFNULL(name, 'Anonymous') +/// Column("name") ?? "Anonymous" +public func ?? (lhs: SQLSpecificExpressible, rhs: SQLExpressible) -> SQLExpression { + return SQLExpressionFunction(.ifNull, arguments: lhs, rhs) +} + + +// MARK: - LENGTH(...) + +extension SQLFunctionName { + /// The `LENGTH` function name + public static let length = SQLFunctionName("LENGTH") +} + +/// Returns an expression that evaluates the `LENGTH` SQL function. +/// +/// // LENGTH(name) +/// length(Column("name")) +public func length(_ value: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionFunction(.length, arguments: value) +} + + +// MARK: - MAX(...) + +extension SQLFunctionName { + /// The `MAX` function name + public static let max = SQLFunctionName("MAX") +} + +/// Returns an expression that evaluates the `MAX` SQL function. +/// +/// // MAX(score) +/// max(Column("score")) +public func max(_ value: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionFunction(.max, arguments: value) +} + + +// MARK: - MIN(...) + +extension SQLFunctionName { + /// The `MIN` function name + public static let min = SQLFunctionName("MIN") +} + +/// Returns an expression that evaluates the `MIN` SQL function. +/// +/// // MIN(score) +/// min(Column("score")) +public func min(_ value: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionFunction(.min, arguments: value) +} + + +// MARK: - SUM(...) + +extension SQLFunctionName { + /// The `SUM` function name + public static let sum = SQLFunctionName("SUM") +} + +/// Returns an expression that evaluates the `SUM` SQL function. +/// +/// // SUM(amount) +/// sum(Column("amount")) +public func sum(_ value: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionFunction(.sum, arguments: value) +} + + +// MARK: - Swift String functions + +extension SQLSpecificExpressible { + /// Returns an SQL expression that applies the Swift's built-in + /// capitalized String property. It is NULL for non-String arguments. + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn.capitalized) + /// let names = try String.fetchAll(dbQueue, request) // [String] + public var capitalized: SQLExpression { + return DatabaseFunction.capitalize.apply(sqlExpression) + } + + /// Returns an SQL expression that applies the Swift's built-in + /// lowercased String property. It is NULL for non-String arguments. + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn.lowercased()) + /// let names = try String.fetchAll(dbQueue, request) // [String] + public var lowercased: SQLExpression { + return DatabaseFunction.lowercase.apply(sqlExpression) + } + + /// Returns an SQL expression that applies the Swift's built-in + /// uppercased String property. It is NULL for non-String arguments. + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn.uppercased()) + /// let names = try String.fetchAll(dbQueue, request) // [String] + public var uppercased: SQLExpression { + return DatabaseFunction.uppercase.apply(sqlExpression) + } +} + +extension SQLSpecificExpressible { + /// Returns an SQL expression that applies the Swift's built-in + /// localizedCapitalized String property. It is NULL for non-String arguments. + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn.localizedCapitalized) + /// let names = try String.fetchAll(dbQueue, request) // [String] + @available(iOS 9.0, OSX 10.11, watchOS 3.0, *) + public var localizedCapitalized: SQLExpression { + return DatabaseFunction.localizedCapitalize.apply(sqlExpression) + } + + /// Returns an SQL expression that applies the Swift's built-in + /// localizedLowercased String property. It is NULL for non-String arguments. + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn.localizedLowercased) + /// let names = try String.fetchAll(dbQueue, request) // [String] + @available(iOS 9.0, OSX 10.11, watchOS 3.0, *) + public var localizedLowercased: SQLExpression { + return DatabaseFunction.localizedLowercase.apply(sqlExpression) + } + + /// Returns an SQL expression that applies the Swift's built-in + /// localizedUppercased String property. It is NULL for non-String arguments. + /// + /// let nameColumn = Column("name") + /// let request = Player.select(nameColumn.localizedUppercased) + /// let names = try String.fetchAll(dbQueue, request) // [String] + @available(iOS 9.0, OSX 10.11, watchOS 3.0, *) + public var localizedUppercased: SQLExpression { + return DatabaseFunction.localizedUppercase.apply(sqlExpression) + } +} diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/Support/SQLOperators.swift b/Pods/GRDB.swift/GRDB/QueryInterface/Support/SQLOperators.swift new file mode 100644 index 0000000..037e14b --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/Support/SQLOperators.swift @@ -0,0 +1,794 @@ +// MARK: - Egality and Identity Operators (=, <>, IS, IS NOT) + +extension SQLBinaryOperator { + /// The `=` binary operator + static let equal = SQLBinaryOperator("=", negated: "<>") + + /// The `<>` binary operator + static let notEqual = SQLBinaryOperator("<>", negated: "=") + + /// The `IS` binary operator + static let `is` = SQLBinaryOperator("IS", negated: "IS NOT") + + /// The `IS NOT` binary operator + static let isNot = SQLBinaryOperator("IS NOT", negated: "IS") +} + +// Outputs "x = y" or "x IS NULL" +private func isEqual(_ lhs: SQLExpression, _ rhs: SQLExpression) -> SQLExpression { + switch (lhs, rhs) { + case (let lhs, let rhs as DatabaseValue): + switch rhs.storage { + case .null: + return SQLExpressionBinary(.is, lhs, DatabaseValue.null) + default: + return SQLExpressionBinary(.equal, lhs, rhs) + } + case (let lhs as DatabaseValue, let rhs): + switch lhs.storage { + case .null: + return SQLExpressionBinary(.is, rhs, DatabaseValue.null) + default: + return SQLExpressionBinary(.equal, lhs, rhs) + } + default: + return SQLExpressionBinary(.equal, lhs, rhs) + } +} + +/// An SQL expression that compares two expressions with the `=` SQL operator. +/// +/// // name = 'Arthur' +/// Column("name") == "Arthur" +/// +/// When the right operand is nil, `IS NULL` is used instead. +/// +/// // name IS NULL +/// Column("name") == nil +public func == (lhs: SQLSpecificExpressible, rhs: SQLExpressible?) -> SQLExpression { + return isEqual(lhs.sqlExpression, rhs?.sqlExpression ?? DatabaseValue.null) +} + +/// An SQL expression that compares two expressions with the `=` SQL operator. +/// +/// // name = 'Arthur' COLLATE NOCASE +/// Column("name").collating(.nocase) == "Arthur" +/// +/// When the right operand is nil, `IS NULL` is used instead. +/// +/// // name IS NULL +/// Column("name").collating(.nocase) == nil +public func == (lhs: SQLCollatedExpression, rhs: SQLExpressible?) -> SQLExpression { + return SQLExpressionCollate(lhs.expression == rhs, collationName: lhs.collationName) +} + +/// An SQL expression that checks the boolean value of an expression. +/// +/// The comparison is done with the built-in boolean evaluation of SQLite: +/// +/// // validated +/// Column("validated") == true +/// +/// // NOT validated +/// Column("validated") == false +public func == (lhs: SQLSpecificExpressible, rhs: Bool) -> SQLExpression { + if rhs { + return lhs.sqlExpression + } else { + return lhs.sqlExpression.negated + } +} + +/// An SQL expression that compares two expressions with the `=` SQL operator. +/// +/// // 'Arthur' = name +/// "Arthur" == Column("name") +/// +/// When the left operand is nil, `IS NULL` is used instead. +/// +/// // name IS NULL +/// nil == Column("name") +public func == (lhs: SQLExpressible?, rhs: SQLSpecificExpressible) -> SQLExpression { + return isEqual(lhs?.sqlExpression ?? DatabaseValue.null, rhs.sqlExpression) +} + +/// An SQL expression that compares two expressions with the `=` SQL operator. +/// +/// // 'Arthur' = name COLLATE NOCASE +/// "Arthur" == Column("name").collating(.nocase) +/// +/// When the left operand is nil, `IS NULL` is used instead. +/// +/// // name IS NULL +/// nil == Column("name").collating(.nocase) +public func == (lhs: SQLExpressible?, rhs: SQLCollatedExpression) -> SQLExpression { + return SQLExpressionCollate(lhs == rhs.expression, collationName: rhs.collationName) +} + +/// An SQL expression that checks the boolean value of an expression. +/// +/// The comparison is done with the built-in boolean evaluation of SQLite: +/// +/// // validated +/// true == Column("validated") +/// +/// // NOT validated +/// false == Column("validated") +public func == (lhs: Bool, rhs: SQLSpecificExpressible) -> SQLExpression { + if lhs { + return rhs.sqlExpression + } else { + return rhs.sqlExpression.negated + } +} + +/// An SQL expression that compares two expressions with the `=` SQL operator. +/// +/// // email = login +/// Column("email") == Column("login") +public func == (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return isEqual(lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL expression that compares two expressions with the `<>` SQL operator. +/// +/// // name <> 'Arthur' +/// Column("name") != "Arthur" +/// +/// When the right operand is nil, `IS NOT NULL` is used instead. +/// +/// // name IS NOT NULL +/// Column("name") != nil +public func != (lhs: SQLSpecificExpressible, rhs: SQLExpressible?) -> SQLExpression { + return isEqual(lhs.sqlExpression, rhs?.sqlExpression ?? DatabaseValue.null).negated +} + +/// An SQL expression that compares two expressions with the `<>` SQL operator. +/// +/// // name <> 'Arthur' COLLATE NOCASE +/// Column("name").collating(.nocase) != "Arthur" +/// +/// When the right operand is nil, `IS NOT NULL` is used instead. +/// +/// // name IS NOT NULL +/// Column("name").collating(.nocase) != nil +public func != (lhs: SQLCollatedExpression, rhs: SQLExpressible?) -> SQLExpression { + return SQLExpressionCollate(lhs.expression != rhs, collationName: lhs.collationName) +} + +/// An SQL expression that checks the boolean value of an expression. +/// +/// The comparison is done with the built-in boolean evaluation of SQLite: +/// +/// // NOT validated +/// Column("validated") != true +/// +/// // validated +/// Column("validated") != false +public func != (lhs: SQLSpecificExpressible, rhs: Bool) -> SQLExpression { + if rhs { + return lhs.sqlExpression.negated + } else { + return lhs.sqlExpression + } +} + +/// An SQL expression that compares two expressions with the `<>` SQL operator. +/// +/// // 'Arthur' <> name +/// "Arthur" != Column("name") +/// +/// When the left operand is nil, `IS NOT NULL` is used instead. +/// +/// // name IS NOT NULL +/// nil != Column("name") +public func != (lhs: SQLExpressible?, rhs: SQLSpecificExpressible) -> SQLExpression { + return isEqual(lhs?.sqlExpression ?? DatabaseValue.null, rhs.sqlExpression).negated +} + +/// An SQL expression that compares two expressions with the `<>` SQL operator. +/// +/// // 'Arthur' <> name COLLATE NOCASE +/// "Arthur" != Column("name").collating(.nocase) +/// +/// When the left operand is nil, `IS NOT NULL` is used instead. +/// +/// // name IS NOT NULL +/// nil != Column("name").collating(.nocase) +public func != (lhs: SQLExpressible?, rhs: SQLCollatedExpression) -> SQLExpression { + return SQLExpressionCollate(lhs != rhs.expression, collationName: rhs.collationName) +} + +/// An SQL expression that checks the boolean value of an expression. +/// +/// The comparison is done with the built-in boolean evaluation of SQLite: +/// +/// // NOT validated +/// true != Column("validated") +/// +/// // validated +/// false != Column("validated") +public func != (lhs: Bool, rhs: SQLSpecificExpressible) -> SQLExpression { + if lhs { + return rhs.sqlExpression.negated + } else { + return rhs.sqlExpression + } +} + +/// An SQL expression that compares two expressions with the `<>` SQL operator. +/// +/// // email <> login +/// Column("email") != Column("login") +public func != (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return isEqual(lhs.sqlExpression, rhs.sqlExpression).negated +} + +/// An SQL expression that compares two expressions with the `IS` SQL operator. +/// +/// // name IS 'Arthur' +/// Column("name") === "Arthur" +public func === (lhs: SQLSpecificExpressible, rhs: SQLExpressible?) -> SQLExpression { + return SQLExpressionBinary(.is, lhs.sqlExpression, rhs?.sqlExpression ?? DatabaseValue.null) +} + +/// An SQL expression that compares two expressions with the `IS` SQL operator. +/// +/// // name IS 'Arthur' COLLATE NOCASE +/// Column("name").collating(.nocase) === "Arthur" +public func === (lhs: SQLCollatedExpression, rhs: SQLExpressible?) -> SQLExpression { + return SQLExpressionCollate(lhs.expression === rhs, collationName: lhs.collationName) +} + +/// An SQL expression that compares two expressions with the `IS` SQL operator. +/// +/// // name IS 'Arthur' +/// "Arthur" === Column("name") +public func === (lhs: SQLExpressible?, rhs: SQLSpecificExpressible) -> SQLExpression { + if let lhs = lhs { + return SQLExpressionBinary(.is, lhs.sqlExpression, rhs.sqlExpression) + } else { + return SQLExpressionBinary(.is, rhs.sqlExpression, DatabaseValue.null) + } +} + +/// An SQL expression that compares two expressions with the `IS` SQL operator. +/// +/// // name IS 'Arthur' COLLATE NOCASE +/// "Arthur" === Column("name").collating(.nocase) +public func === (lhs: SQLExpressible?, rhs: SQLCollatedExpression) -> SQLExpression { + return SQLExpressionCollate(lhs === rhs.expression, collationName: rhs.collationName) +} + +/// An SQL expression that compares two expressions with the `IS` SQL operator. +/// +/// // email IS login +/// Column("email") === Column("login") +public func === (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.is, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL expression that compares two expressions with the `IS NOT` SQL operator. +/// +/// // name IS NOT 'Arthur' +/// Column("name") !== "Arthur" +public func !== (lhs: SQLSpecificExpressible, rhs: SQLExpressible?) -> SQLExpression { + return SQLExpressionBinary(.isNot, lhs.sqlExpression, rhs?.sqlExpression ?? DatabaseValue.null) +} + +/// An SQL expression that compares two expressions with the `IS NOT` SQL operator. +/// +/// // name IS NOT 'Arthur' COLLATE NOCASE +/// Column("name").collating(.nocase) !== "Arthur" +public func !== (lhs: SQLCollatedExpression, rhs: SQLExpressible?) -> SQLExpression { + return SQLExpressionCollate(lhs.expression !== rhs, collationName: lhs.collationName) +} + +/// An SQL expression that compares two expressions with the `IS NOT` SQL operator. +/// +/// // name IS NOT 'Arthur' +/// "Arthur" !== Column("name") +public func !== (lhs: SQLExpressible?, rhs: SQLSpecificExpressible) -> SQLExpression { + if let lhs = lhs { + return SQLExpressionBinary(.isNot, lhs.sqlExpression, rhs.sqlExpression) + } else { + return SQLExpressionBinary(.isNot, rhs.sqlExpression, DatabaseValue.null) + } +} + +/// An SQL expression that compares two expressions with the `IS NOT` SQL operator. +/// +/// // name IS NOT 'Arthur' COLLATE NOCASE +/// "Arthur" !== Column("name").collating(.nocase) +public func !== (lhs: SQLExpressible?, rhs: SQLCollatedExpression) -> SQLExpression { + return SQLExpressionCollate(lhs !== rhs.expression, collationName: rhs.collationName) +} + +/// An SQL expression that compares two expressions with the `IS NOT` SQL operator. +/// +/// // email IS NOT login +/// Column("email") !== Column("login") +public func !== (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.isNot, lhs.sqlExpression, rhs.sqlExpression) +} + + +// MARK: - Comparison Operators (<, >, <=, >=) + +extension SQLBinaryOperator { + /// The `<` binary operator + static let lessThan = SQLBinaryOperator("<") + + /// The `<=` binary operator + static let lessThanOrEqual = SQLBinaryOperator("<=") + + /// The `>` binary operator + static let greaterThan = SQLBinaryOperator(">") + + /// The `>=` binary operator + static let greaterThanOrEqual = SQLBinaryOperator(">=") +} + +/// An SQL expression that compares two expressions with the `<` SQL operator. +/// +/// // score < 18 +/// Column("score") < 18 +public func < (lhs: SQLSpecificExpressible, rhs: SQLExpressible) -> SQLExpression { + return SQLExpressionBinary(.lessThan, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL expression that compares two expressions with the `<` SQL operator. +/// +/// // name < 'Arthur' COLLATE NOCASE +/// Column("name").collating(.nocase) < "Arthur" +public func < (lhs: SQLCollatedExpression, rhs: SQLExpressible) -> SQLExpression { + return SQLExpressionCollate(lhs.expression < rhs, collationName: lhs.collationName) +} + +/// An SQL expression that compares two expressions with the `<` SQL operator. +/// +/// // 18 < score +/// 18 < Column("score") +public func < (lhs: SQLExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.lessThan, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL expression that compares two expressions with the `<` SQL operator. +/// +/// // 'Arthur' < name COLLATE NOCASE +/// "Arthur" < Column("name").collating(.nocase) +public func < (lhs: SQLExpressible, rhs: SQLCollatedExpression) -> SQLExpression { + return SQLExpressionCollate(lhs < rhs.expression, collationName: rhs.collationName) +} + +/// An SQL expression that compares two expressions with the `<` SQL operator. +/// +/// // width < height +/// Column("width") < Column("height") +public func < (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.lessThan, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL expression that compares two expressions with the `<=` SQL operator. +/// +/// // score <= 18 +/// Column("score") <= 18 +public func <= (lhs: SQLSpecificExpressible, rhs: SQLExpressible) -> SQLExpression { + return SQLExpressionBinary(.lessThanOrEqual, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL expression that compares two expressions with the `<=` SQL operator. +/// +/// // name <= 'Arthur' COLLATE NOCASE +/// Column("name").collating(.nocase) <= "Arthur" +public func <= (lhs: SQLCollatedExpression, rhs: SQLExpressible) -> SQLExpression { + return SQLExpressionCollate(lhs.expression <= rhs, collationName: lhs.collationName) +} + +/// An SQL expression that compares two expressions with the `<=` SQL operator. +/// +/// // 18 <= score +/// 18 <= Column("score") +public func <= (lhs: SQLExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.lessThanOrEqual, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL expression that compares two expressions with the `<=` SQL operator. +/// +/// // 'Arthur' <= name COLLATE NOCASE +/// "Arthur" <= Column("name").collating(.nocase) +public func <= (lhs: SQLExpressible, rhs: SQLCollatedExpression) -> SQLExpression { + return SQLExpressionCollate(lhs <= rhs.expression, collationName: rhs.collationName) +} + +/// An SQL expression that compares two expressions with the `<=` SQL operator. +/// +/// // width <= height +/// Column("width") <= Column("height") +public func <= (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.lessThanOrEqual, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL expression that compares two expressions with the `>` SQL operator. +/// +/// // score > 18 +/// Column("score") > 18 +public func > (lhs: SQLSpecificExpressible, rhs: SQLExpressible) -> SQLExpression { + return SQLExpressionBinary(.greaterThan, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL expression that compares two expressions with the `>` SQL operator. +/// +/// // name > 'Arthur' COLLATE NOCASE +/// Column("name").collating(.nocase) > "Arthur" +public func > (lhs: SQLCollatedExpression, rhs: SQLExpressible) -> SQLExpression { + return SQLExpressionCollate(lhs.expression > rhs, collationName: lhs.collationName) +} + +/// An SQL expression that compares two expressions with the `>` SQL operator. +/// +/// // 18 > score +/// 18 > Column("score") +public func > (lhs: SQLExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.greaterThan, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL expression that compares two expressions with the `>` SQL operator. +/// +/// // 'Arthur' > name COLLATE NOCASE +/// "Arthur" > Column("name").collating(.nocase) +public func > (lhs: SQLExpressible, rhs: SQLCollatedExpression) -> SQLExpression { + return SQLExpressionCollate(lhs > rhs.expression, collationName: rhs.collationName) +} + +/// An SQL expression that compares two expressions with the `>` SQL operator. +/// +/// // width > height +/// Column("width") > Column("height") +public func > (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.greaterThan, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL expression that compares two expressions with the `>=` SQL operator. +/// +/// // score >= 18 +/// Column("score") >= 18 +public func >= (lhs: SQLSpecificExpressible, rhs: SQLExpressible) -> SQLExpression { + return SQLExpressionBinary(.greaterThanOrEqual, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL expression that compares two expressions with the `>=` SQL operator. +/// +/// // name >= 'Arthur' COLLATE NOCASE +/// Column("name").collating(.nocase) >= "Arthur" +public func >= (lhs: SQLCollatedExpression, rhs: SQLExpressible) -> SQLExpression { + return SQLExpressionCollate(lhs.expression >= rhs, collationName: lhs.collationName) +} + +/// An SQL expression that compares two expressions with the `>=` SQL operator. +/// +/// // 18 >= score +/// 18 >= Column("score") +public func >= (lhs: SQLExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.greaterThanOrEqual, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL expression that compares two expressions with the `>=` SQL operator. +/// +/// // 'Arthur' >= name COLLATE NOCASE +/// "Arthur" >= Column("name").collating(.nocase) +public func >= (lhs: SQLExpressible, rhs: SQLCollatedExpression) -> SQLExpression { + return SQLExpressionCollate(lhs >= rhs.expression, collationName: rhs.collationName) +} + +/// An SQL expression that compares two expressions with the `>=` SQL operator. +/// +/// // width >= height +/// Column("width") >= Column("height") +public func >= (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.greaterThanOrEqual, lhs.sqlExpression, rhs.sqlExpression) +} + + +// MARK: - Inclusion Operators (BETWEEN, IN) + +extension Range where Bound: SQLExpressible { + /// An SQL expression that checks the inclusion of an expression in a range. + /// + /// // email >= 'A' AND email < 'B' + /// ("A"..<"B").contains(Column("email")) + public func contains(_ element: SQLSpecificExpressible) -> SQLExpression { + return (element >= lowerBound) && (element < upperBound) + } + + /// An SQL expression that checks the inclusion of an expression in a range. + /// + /// // email >= 'A' COLLATE NOCASE AND email < 'B' COLLATE NOCASE + /// ("A"..<"B").contains(Column("email").collating(.nocase)) + public func contains(_ element: SQLCollatedExpression) -> SQLExpression { + return (element >= lowerBound) && (element < upperBound) + } +} + +extension ClosedRange where Bound: SQLExpressible { + /// An SQL expression that checks the inclusion of an expression in a range. + /// + /// // email BETWEEN 'A' AND 'B' + /// ("A"..."B").contains(Column("email")) + public func contains(_ element: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBetween(element.sqlExpression, lowerBound.sqlExpression, upperBound.sqlExpression) + } + + /// An SQL expression that checks the inclusion of an expression in a range. + /// + /// // email BETWEEN 'A' AND 'B' COLLATE NOCASE + /// ("A"..."B").contains(Column("email").collating(.nocase)) + public func contains(_ element: SQLCollatedExpression) -> SQLExpression { + return SQLExpressionCollate(contains(element.expression), collationName: element.collationName) + } +} + +extension CountableRange where Bound: SQLExpressible { + /// An SQL expression that checks the inclusion of an expression in a range. + /// + /// // id BETWEEN 1 AND 9 + /// (1..<10).contains(Column("id")) + public func contains(_ element: SQLSpecificExpressible) -> SQLExpression { + return (element >= lowerBound) && (element < upperBound) + } +} + +extension CountableClosedRange where Bound: SQLExpressible { + /// An SQL expression that checks the inclusion of an expression in a range. + /// + /// // id BETWEEN 1 AND 10 + /// (1...10).contains(Column("id")) + public func contains(_ element: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBetween(element.sqlExpression, lowerBound.sqlExpression, upperBound.sqlExpression) + } +} + +extension Sequence where Self.Iterator.Element: SQLExpressible { + /// An SQL expression that checks the inclusion of an expression in + /// a sequence. + /// + /// // id IN (1,2,3) + /// [1, 2, 3].contains(Column("id")) + public func contains(_ element: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionsArray(self).contains(element.sqlExpression) + } + + /// An SQL expression that checks the inclusion of an expression in + /// a sequence. + /// + /// // name IN ('A', 'B') COLLATE NOCASE + /// ["A", "B"].contains(Column("name").collating(.nocase)) + public func contains(_ element: SQLCollatedExpression) -> SQLExpression { + return SQLExpressionCollate(contains(element.expression), collationName: element.collationName) + } +} + + +// MARK: - Arithmetic Operators (+, -, *, /) + +extension SQLBinaryOperator { + /// The `+` binary operator + static let plus = SQLBinaryOperator("+") + + /// The `-` binary operator + static let minus = SQLBinaryOperator("-") + + /// The `*` binary operator + static let multiply = SQLBinaryOperator("*") + + /// The `/` binary operator + static let divide = SQLBinaryOperator("/") +} + +extension SQLUnaryOperator { + /// The `-` unary operator + public static let minus = SQLUnaryOperator("-", needsRightSpace: false) +} + +/// An SQL arithmetic multiplication. +/// +/// // width * 2 +/// Column("width") * 2 +public func * (lhs: SQLSpecificExpressible, rhs: SQLExpressible) -> SQLExpression { + return SQLExpressionBinary(.multiply, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL arithmetic multiplication. +/// +/// // 2 * width +/// 2 * Column("width") +public func * (lhs: SQLExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.multiply, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL arithmetic multiplication. +/// +/// // width * height +/// Column("width") * Column("height") +public func * (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.multiply, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL arithmetic division. +/// +/// // width / 2 +/// Column("width") / 2 +public func / (lhs: SQLSpecificExpressible, rhs: SQLExpressible) -> SQLExpression { + return SQLExpressionBinary(.divide, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL arithmetic division. +/// +/// // 2 / width +/// 2 / Column("width") +public func / (lhs: SQLExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.divide, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL arithmetic division. +/// +/// // width / height +/// Column("width") / Column("height") +public func / (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.divide, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL arithmetic addition. +/// +/// // width + 2 +/// Column("width") + 2 +public func + (lhs: SQLSpecificExpressible, rhs: SQLExpressible) -> SQLExpression { + return SQLExpressionBinary(.plus, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL arithmetic addition. +/// +/// // 2 + width +/// 2 + Column("width") +public func + (lhs: SQLExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.plus, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL arithmetic addition. +/// +/// // width + height +/// Column("width") + Column("height") +public func + (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.plus, lhs.sqlExpression, rhs.sqlExpression) +} + +/// A negated SQL arithmetic expression. +/// +/// // -width +/// -Column("width") +public prefix func - (value: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionUnary(.minus, value.sqlExpression) +} + +/// An SQL arithmetic substraction. +/// +/// // width - 2 +/// Column("width") - 2 +public func - (lhs: SQLSpecificExpressible, rhs: SQLExpressible) -> SQLExpression { + return SQLExpressionBinary(.minus, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL arithmetic substraction. +/// +/// // 2 - width +/// 2 - Column("width") +public func - (lhs: SQLExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.minus, lhs.sqlExpression, rhs.sqlExpression) +} + +/// An SQL arithmetic substraction. +/// +/// // width - height +/// Column("width") - Column("height") +public func - (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.minus, lhs.sqlExpression, rhs.sqlExpression) +} + + +// MARK: - Logical Operators (AND, OR, NOT) + +extension SQLBinaryOperator { + /// The `AND` binary operator + static let and = SQLBinaryOperator("AND") + + /// The `OR` binary operator + static let or = SQLBinaryOperator("OR") +} + +/// A logical SQL expression with the `AND` SQL operator. +/// +/// // favorite AND 0 +/// Column("favorite") && false +public func && (lhs: SQLSpecificExpressible, rhs: SQLExpressible) -> SQLExpression { + return SQLExpressionBinary(.and, lhs.sqlExpression, rhs.sqlExpression) +} + +/// A logical SQL expression with the `AND` SQL operator. +/// +/// // 0 AND favorite +/// false && Column("favorite") +public func && (lhs: SQLExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.and, lhs.sqlExpression, rhs.sqlExpression) +} + +/// A logical SQL expression with the `AND` SQL operator. +/// +/// // email IS NOT NULL AND favorite +/// Column("email") != nil && Column("favorite") +public func && (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.and, lhs.sqlExpression, rhs.sqlExpression) +} + +/// A logical SQL expression with the `OR` SQL operator. +/// +/// // favorite OR 1 +/// Column("favorite") || true +public func || (lhs: SQLSpecificExpressible, rhs: SQLExpressible) -> SQLExpression { + return SQLExpressionBinary(.or, lhs.sqlExpression, rhs.sqlExpression) +} + +/// A logical SQL expression with the `OR` SQL operator. +/// +/// // 0 OR favorite +/// true || Column("favorite") +public func || (lhs: SQLExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.or, lhs.sqlExpression, rhs.sqlExpression) +} + +/// A logical SQL expression with the `OR` SQL operator. +/// +/// // email IS NULL OR hidden +/// Column("email") == nil || Column("hidden") +public func || (lhs: SQLSpecificExpressible, rhs: SQLSpecificExpressible) -> SQLExpression { + return SQLExpressionBinary(.or, lhs.sqlExpression, rhs.sqlExpression) +} + +/// A negated logical SQL expression with the `NOT` SQL operator. +/// +/// // NOT hidden +/// !Column("hidden") +/// +/// Some expressions may be negated with specific SQL operators: +/// +/// // id NOT BETWEEN 1 AND 10 +/// !((1...10).contains(Column("id"))) +public prefix func ! (value: SQLSpecificExpressible) -> SQLExpression { + return value.sqlExpression.negated +} + + +// MARK: - Like Operator + +extension SQLBinaryOperator { + /// The `LIKE` binary operator + static let like = SQLBinaryOperator("LIKE") +} + +extension SQLSpecificExpressible { + + /// An SQL expression with the `LIKE` SQL operator. + /// + /// // email LIKE '%@example.com" + /// Column("email").like("%@example.com") + public func like(_ pattern: SQLExpressible) -> SQLExpression { + return SQLExpressionBinary(.like, self, pattern) + } +} + + +// MARK: - Match Operator + +extension SQLBinaryOperator { + /// The `MATCH` binary operator + static let match = SQLBinaryOperator("MATCH") +} diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/TableDefinition.swift b/Pods/GRDB.swift/GRDB/QueryInterface/TableDefinition.swift new file mode 100644 index 0000000..c658dee --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/TableDefinition.swift @@ -0,0 +1,882 @@ +extension Database { + + // MARK: - Database Schema + + #if GRDBCUSTOMSQLITE || GRDBCIPHER + /// Creates a database table. + /// + /// try db.create(table: "pointOfInterests") { t in + /// t.column("id", .integer).primaryKey() + /// t.column("title", .text) + /// t.column("favorite", .boolean).notNull().default(false) + /// t.column("longitude", .double).notNull() + /// t.column("latitude", .double).notNull() + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html and + /// https://www.sqlite.org/withoutrowid.html + /// + /// - parameters: + /// - name: The table name. + /// - temporary: If true, creates a temporary table. + /// - ifNotExists: If false, no error is thrown if table already exists. + /// - withoutRowID: If true, uses WITHOUT ROWID optimization. + /// - body: A closure that defines table columns and constraints. + /// - throws: A DatabaseError whenever an SQLite error occurs. + public func create(table name: String, temporary: Bool = false, ifNotExists: Bool = false, withoutRowID: Bool = false, body: (TableDefinition) -> Void) throws { + let definition = TableDefinition(name: name, temporary: temporary, ifNotExists: ifNotExists, withoutRowID: withoutRowID) + body(definition) + let sql = try definition.sql(self) + try execute(sql) + } + #else + /// Creates a database table. + /// + /// try db.create(table: "pointOfInterests") { t in + /// t.column("id", .integer).primaryKey() + /// t.column("title", .text) + /// t.column("favorite", .boolean).notNull().default(false) + /// t.column("longitude", .double).notNull() + /// t.column("latitude", .double).notNull() + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html and + /// https://www.sqlite.org/withoutrowid.html + /// + /// - parameters: + /// - name: The table name. + /// - temporary: If true, creates a temporary table. + /// - ifNotExists: If false, no error is thrown if table already exists. + /// - withoutRowID: If true, uses WITHOUT ROWID optimization. + /// - body: A closure that defines table columns and constraints. + /// - throws: A DatabaseError whenever an SQLite error occurs. + @available(iOS 8.2, OSX 10.10, *) + public func create(table name: String, temporary: Bool = false, ifNotExists: Bool = false, withoutRowID: Bool, body: (TableDefinition) -> Void) throws { + // WITHOUT ROWID was added in SQLite 3.8.2 http://www.sqlite.org/changes.html#version_3_8_2 + // It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS) + let definition = TableDefinition(name: name, temporary: temporary, ifNotExists: ifNotExists, withoutRowID: withoutRowID) + body(definition) + let sql = try definition.sql(self) + try execute(sql) + } + + /// Creates a database table. + /// + /// try db.create(table: "pointOfInterests") { t in + /// t.column("id", .integer).primaryKey() + /// t.column("title", .text) + /// t.column("favorite", .boolean).notNull().default(false) + /// t.column("longitude", .double).notNull() + /// t.column("latitude", .double).notNull() + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html + /// + /// - parameters: + /// - name: The table name. + /// - temporary: If true, creates a temporary table. + /// - ifNotExists: If false, no error is thrown if table already exists. + /// - body: A closure that defines table columns and constraints. + /// - throws: A DatabaseError whenever an SQLite error occurs. + public func create(table name: String, temporary: Bool = false, ifNotExists: Bool = false, body: (TableDefinition) -> Void) throws { + let definition = TableDefinition(name: name, temporary: temporary, ifNotExists: ifNotExists, withoutRowID: false) + body(definition) + let sql = try definition.sql(self) + try execute(sql) + } + #endif + + /// Renames a database table. + /// + /// See https://www.sqlite.org/lang_altertable.html + /// + /// - throws: A DatabaseError whenever an SQLite error occurs. + public func rename(table name: String, to newName: String) throws { + try execute("ALTER TABLE \(name.quotedDatabaseIdentifier) RENAME TO \(newName.quotedDatabaseIdentifier)") + } + + /// Modifies a database table. + /// + /// try db.alter(table: "players") { t in + /// t.add(column: "url", .text) + /// } + /// + /// See https://www.sqlite.org/lang_altertable.html + /// + /// - parameters: + /// - name: The table name. + /// - body: A closure that defines table alterations. + /// - throws: A DatabaseError whenever an SQLite error occurs. + public func alter(table name: String, body: (TableAlteration) -> Void) throws { + let alteration = TableAlteration(name: name) + body(alteration) + let sql = try alteration.sql(self) + try execute(sql) + } + + /// Deletes a database table. + /// + /// See https://www.sqlite.org/lang_droptable.html + /// + /// - throws: A DatabaseError whenever an SQLite error occurs. + public func drop(table name: String) throws { + try execute("DROP TABLE \(name.quotedDatabaseIdentifier)") + } + + #if GRDBCUSTOMSQLITE || GRDBCIPHER + /// Creates an index. + /// + /// try db.create(index: "playerByEmail", on: "player", columns: ["email"]) + /// + /// SQLite can also index expressions (https://www.sqlite.org/expridx.html) + /// and use specific collations. To create such an index, use a raw SQL + /// query. + /// + /// try db.execute("CREATE INDEX ...") + /// + /// See https://www.sqlite.org/lang_createindex.html + /// + /// - parameters: + /// - name: The index name. + /// - table: The name of the indexed table. + /// - columns: The indexed columns. + /// - unique: If true, creates a unique index. + /// - ifNotExists: If false, no error is thrown if index already exists. + /// - condition: If not nil, creates a partial index + /// (see https://www.sqlite.org/partialindex.html). + public func create(index name: String, on table: String, columns: [String], unique: Bool = false, ifNotExists: Bool = false, condition: SQLExpressible? = nil) throws { + // Partial indexes were introduced in SQLite 3.8.0 http://www.sqlite.org/changes.html#version_3_8_0 + // It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS) + let definition = IndexDefinition(name: name, table: table, columns: columns, unique: unique, ifNotExists: ifNotExists, condition: condition?.sqlExpression) + let sql = definition.sql() + try execute(sql) + } + #else + /// Creates an index. + /// + /// try db.create(index: "playerByEmail", on: "player", columns: ["email"]) + /// + /// SQLite can also index expressions (https://www.sqlite.org/expridx.html) + /// and use specific collations. To create such an index, use a raw SQL + /// query. + /// + /// try db.execute("CREATE INDEX ...") + /// + /// See https://www.sqlite.org/lang_createindex.html + /// + /// - parameters: + /// - name: The index name. + /// - table: The name of the indexed table. + /// - columns: The indexed columns. + /// - unique: If true, creates a unique index. + /// - ifNotExists: If false, no error is thrown if index already exists. + public func create(index name: String, on table: String, columns: [String], unique: Bool = false, ifNotExists: Bool = false) throws { + // Partial indexes were introduced in SQLite 3.8.0 http://www.sqlite.org/changes.html#version_3_8_0 + // It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS) + let definition = IndexDefinition(name: name, table: table, columns: columns, unique: unique, ifNotExists: ifNotExists, condition: nil) + let sql = definition.sql() + try execute(sql) + } + + /// Creates a partial index. + /// + /// try db.create(index: "playerByEmail", on: "player", columns: ["email"], condition: Column("email") != nil) + /// + /// See https://www.sqlite.org/lang_createindex.html, and + /// https://www.sqlite.org/partialindex.html + /// + /// - parameters: + /// - name: The index name. + /// - table: The name of the indexed table. + /// - columns: The indexed columns. + /// - unique: If true, creates a unique index. + /// - ifNotExists: If false, no error is thrown if index already exists. + /// - condition: The condition that indexed rows must verify. + @available(iOS 8.2, OSX 10.10, *) + public func create(index name: String, on table: String, columns: [String], unique: Bool = false, ifNotExists: Bool = false, condition: SQLExpressible) throws { + // Partial indexes were introduced in SQLite 3.8.0 http://www.sqlite.org/changes.html#version_3_8_0 + // It is available from iOS 8.2 and OS X 10.10 https://github.com/yapstudios/YapDatabase/wiki/SQLite-version-(bundled-with-OS) + let definition = IndexDefinition(name: name, table: table, columns: columns, unique: unique, ifNotExists: ifNotExists, condition: condition.sqlExpression) + let sql = definition.sql() + try execute(sql) + } + #endif + + /// Deletes a database index. + /// + /// See https://www.sqlite.org/lang_dropindex.html + /// + /// - throws: A DatabaseError whenever an SQLite error occurs. + public func drop(index name: String) throws { + try execute("DROP INDEX \(name.quotedDatabaseIdentifier)") + } +} + +/// The TableDefinition class lets you define table columns and constraints. +/// +/// You don't create instances of this class. Instead, you use the Database +/// `create(table:)` method: +/// +/// try db.create(table: "players") { t in // t is TableDefinition +/// t.column(...) +/// } +/// +/// See https://www.sqlite.org/lang_createtable.html +public final class TableDefinition { + private typealias KeyConstraint = (columns: [String], conflictResolution: Database.ConflictResolution?) + + private let name: String + private let temporary: Bool + private let ifNotExists: Bool + private let withoutRowID: Bool + private var columns: [ColumnDefinition] = [] + private var primaryKeyConstraint: KeyConstraint? + private var uniqueKeyConstraints: [KeyConstraint] = [] + private var foreignKeyConstraints: [(columns: [String], table: String, destinationColumns: [String]?, deleteAction: Database.ForeignKeyAction?, updateAction: Database.ForeignKeyAction?, deferred: Bool)] = [] + private var checkConstraints: [SQLExpression] = [] + + init(name: String, temporary: Bool, ifNotExists: Bool, withoutRowID: Bool) { + self.name = name + self.temporary = temporary + self.ifNotExists = ifNotExists + self.withoutRowID = withoutRowID + } + + /// Appends a table column. + /// + /// try db.create(table: "players") { t in + /// t.column("name", .text) + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html#tablecoldef + /// + /// - parameter name: the column name. + /// - parameter type: the eventual column type. + /// - returns: An ColumnDefinition that allows you to refine the + /// column definition. + @discardableResult + public func column(_ name: String, _ type: Database.ColumnType? = nil) -> ColumnDefinition { + let column = ColumnDefinition(name: name, type: type) + columns.append(column) + return column + } + + /// Defines the table primary key. + /// + /// try db.create(table: "citizenships") { t in + /// t.column("citizenID", .integer) + /// t.column("countryCode", .text) + /// t.primaryKey(["citizenID", "countryCode"]) + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html#primkeyconst and + /// https://www.sqlite.org/lang_createtable.html#rowid + /// + /// - parameter columns: The primary key columns. + /// - parameter conflitResolution: An optional conflict resolution + /// (see https://www.sqlite.org/lang_conflict.html). + public func primaryKey(_ columns: [String], onConflict conflictResolution: Database.ConflictResolution? = nil) { + guard primaryKeyConstraint == nil else { + // Programmer error + fatalError("can't define several primary keys") + } + primaryKeyConstraint = (columns: columns, conflictResolution: conflictResolution) + } + + /// Adds a unique key. + /// + /// try db.create(table: "pointOfInterests") { t in + /// t.column("latitude", .double) + /// t.column("longitude", .double) + /// t.uniqueKey(["latitude", "longitude"]) + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html#uniqueconst + /// + /// - parameter columns: The unique key columns. + /// - parameter conflitResolution: An optional conflict resolution + /// (see https://www.sqlite.org/lang_conflict.html). + public func uniqueKey(_ columns: [String], onConflict conflictResolution: Database.ConflictResolution? = nil) { + uniqueKeyConstraints.append((columns: columns, conflictResolution: conflictResolution)) + } + + /// Adds a foreign key. + /// + /// try db.create(table: "passport") { t in + /// t.column("issueDate", .date) + /// t.column("citizenID", .integer) + /// t.column("countryCode", .text) + /// t.foreignKey(["citizenID", "countryCode"], references: "citizenships", onDelete: .cascade) + /// } + /// + /// See https://www.sqlite.org/foreignkeys.html + /// + /// - parameters: + /// - columns: The foreign key columns. + /// - table: The referenced table. + /// - destinationColumns: The columns in the referenced table. If not + /// specified, the columns of the primary key of the referenced table + /// are used. + /// - deleteAction: Optional action when the referenced row is deleted. + /// - updateAction: Optional action when the referenced row is updated. + /// - deferred: If true, defines a deferred foreign key constraint. + /// See https://www.sqlite.org/foreignkeys.html#fk_deferred. + public func foreignKey(_ columns: [String], references table: String, columns destinationColumns: [String]? = nil, onDelete deleteAction: Database.ForeignKeyAction? = nil, onUpdate updateAction: Database.ForeignKeyAction? = nil, deferred: Bool = false) { + foreignKeyConstraints.append((columns: columns, table: table, destinationColumns: destinationColumns, deleteAction: deleteAction, updateAction: updateAction, deferred: deferred)) + } + + /// Adds a CHECK constraint. + /// + /// try db.create(table: "players") { t in + /// t.column("personalPhone", .text) + /// t.column("workPhone", .text) + /// let personalPhone = Column("personalPhone") + /// let workPhone = Column("workPhone") + /// t.check(personalPhone != nil || workPhone != nil) + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html#ckconst + /// + /// - parameter condition: The checked condition + public func check(_ condition: SQLExpressible) { + checkConstraints.append(condition.sqlExpression) + } + + /// Adds a CHECK constraint. + /// + /// try db.create(table: "players") { t in + /// t.column("personalPhone", .text) + /// t.column("workPhone", .text) + /// t.check(sql: "personalPhone IS NOT NULL OR workPhone IS NOT NULL") + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html#ckconst + /// + /// - parameter sql: An SQL snippet + public func check(sql: String) { + checkConstraints.append(SQLExpressionLiteral(sql)) + } + + fileprivate func sql(_ db: Database) throws -> String { + var statements: [String] = [] + + do { + var chunks: [String] = [] + chunks.append("CREATE") + if temporary { + chunks.append("TEMPORARY") + } + chunks.append("TABLE") + if ifNotExists { + chunks.append("IF NOT EXISTS") + } + chunks.append(name.quotedDatabaseIdentifier) + + let primaryKeyColumns: [String] + if let (columns, _) = primaryKeyConstraint { + primaryKeyColumns = columns + } else if let index = columns.index(where: { $0.primaryKey != nil }) { + primaryKeyColumns = [columns[index].name] + } else { + // WITHOUT ROWID optimization requires a primary key. If the + // user sets withoutRowID, but does not define a primary key, + // this is undefined behavior. + // + // We thus can use the rowId column even when the withoutRowID + // flag is set ;-) + primaryKeyColumns = [Column.rowID.name] + } + + do { + var items: [String] = [] + try items.append(contentsOf: columns.map { try $0.sql(db, tableName: name, primaryKeyColumns: primaryKeyColumns) }) + + if let (columns, conflictResolution) = primaryKeyConstraint { + var chunks: [String] = [] + chunks.append("PRIMARY KEY") + chunks.append("(\((columns.map { $0.quotedDatabaseIdentifier } as [String]).joined(separator: ", ")))") + if let conflictResolution = conflictResolution { + chunks.append("ON CONFLICT") + chunks.append(conflictResolution.rawValue) + } + items.append(chunks.joined(separator: " ")) + } + + for (columns, conflictResolution) in uniqueKeyConstraints { + var chunks: [String] = [] + chunks.append("UNIQUE") + chunks.append("(\((columns.map { $0.quotedDatabaseIdentifier } as [String]).joined(separator: ", ")))") + if let conflictResolution = conflictResolution { + chunks.append("ON CONFLICT") + chunks.append(conflictResolution.rawValue) + } + items.append(chunks.joined(separator: " ")) + } + + for (columns, table, destinationColumns, deleteAction, updateAction, deferred) in foreignKeyConstraints { + var chunks: [String] = [] + chunks.append("FOREIGN KEY") + chunks.append("(\((columns.map { $0.quotedDatabaseIdentifier } as [String]).joined(separator: ", ")))") + chunks.append("REFERENCES") + if let destinationColumns = destinationColumns { + chunks.append("\(table.quotedDatabaseIdentifier)(\((destinationColumns.map { $0.quotedDatabaseIdentifier } as [String]).joined(separator: ", ")))") + } else if table == name { + chunks.append("\(table.quotedDatabaseIdentifier)(\((primaryKeyColumns.map { $0.quotedDatabaseIdentifier } as [String]).joined(separator: ", ")))") + } else { + let primaryKey = try db.primaryKey(table) + chunks.append("\(table.quotedDatabaseIdentifier)(\((primaryKey.columns.map { $0.quotedDatabaseIdentifier } as [String]).joined(separator: ", ")))") + } + if let deleteAction = deleteAction { + chunks.append("ON DELETE") + chunks.append(deleteAction.rawValue) + } + if let updateAction = updateAction { + chunks.append("ON UPDATE") + chunks.append(updateAction.rawValue) + } + if deferred { + chunks.append("DEFERRABLE INITIALLY DEFERRED") + } + items.append(chunks.joined(separator: " ")) + } + + for checkExpression in checkConstraints { + var chunks: [String] = [] + chunks.append("CHECK") + chunks.append("(" + checkExpression.sql + ")") + items.append(chunks.joined(separator: " ")) + } + + chunks.append("(\(items.joined(separator: ", ")))") + } + + if withoutRowID { + chunks.append("WITHOUT ROWID") + } + statements.append(chunks.joined(separator: " ")) + } + + let indexStatements = columns + .flatMap { $0.indexDefinition(in: name) } + .map { $0.sql() } + statements.append(contentsOf: indexStatements) + return statements.joined(separator: "; ") + } +} + +/// The TableAlteration class lets you alter database tables. +/// +/// You don't create instances of this class. Instead, you use the Database +/// `alter(table:)` method: +/// +/// try db.alter(table: "players") { t in // t is TableAlteration +/// t.add(column: ...) +/// } +/// +/// See https://www.sqlite.org/lang_altertable.html +public final class TableAlteration { + private let name: String + private var addedColumns: [ColumnDefinition] = [] + + init(name: String) { + self.name = name + } + + /// Appends a column to the table. + /// + /// try db.alter(table: "players") { t in + /// t.add(column: "url", .text) + /// } + /// + /// See https://www.sqlite.org/lang_altertable.html + /// + /// - parameter name: the column name. + /// - parameter type: the column type. + /// - returns: An ColumnDefinition that allows you to refine the + /// column definition. + @discardableResult + public func add(column name: String, _ type: Database.ColumnType) -> ColumnDefinition { + let column = ColumnDefinition(name: name, type: type) + addedColumns.append(column) + return column + } + + fileprivate func sql(_ db: Database) throws -> String { + var statements: [String] = [] + + for column in addedColumns { + var chunks: [String] = [] + chunks.append("ALTER TABLE") + chunks.append(name.quotedDatabaseIdentifier) + chunks.append("ADD COLUMN") + try chunks.append(column.sql(db, tableName: name, primaryKeyColumns: nil)) + let statement = chunks.joined(separator: " ") + statements.append(statement) + + if let indexDefinition = column.indexDefinition(in: name) { + statements.append(indexDefinition.sql()) + } + } + + return statements.joined(separator: "; ") + } +} + +/// The ColumnDefinition class lets you refine a table column. +/// +/// You get instances of this class when you create or alter a database table: +/// +/// try db.create(table: "players") { t in +/// t.column(...) // ColumnDefinition +/// } +/// +/// try db.alter(table: "players") { t in +/// t.add(column: ...) // ColumnDefinition +/// } +/// +/// See https://www.sqlite.org/lang_createtable.html and +/// https://www.sqlite.org/lang_altertable.html +public final class ColumnDefinition { + enum Index { + case none + case index + case unique(Database.ConflictResolution) + } + fileprivate let name: String + private let type: Database.ColumnType? + fileprivate var primaryKey: (conflictResolution: Database.ConflictResolution?, autoincrement: Bool)? + private var index: Index = .none + private var notNullConflictResolution: Database.ConflictResolution? + private var checkConstraints: [SQLExpression] = [] + private var foreignKeyConstraints: [(table: String, column: String?, deleteAction: Database.ForeignKeyAction?, updateAction: Database.ForeignKeyAction?, deferred: Bool)] = [] + private var defaultExpression: SQLExpression? + private var collationName: String? + + init(name: String, type: Database.ColumnType?) { + self.name = name + self.type = type + } + + /// Adds a primary key constraint on the column. + /// + /// try db.create(table: "players") { t in + /// t.column("id", .integer).primaryKey() + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html#primkeyconst and + /// https://www.sqlite.org/lang_createtable.html#rowid + /// + /// - parameters: + /// - conflitResolution: An optional conflict resolution + /// (see https://www.sqlite.org/lang_conflict.html). + /// - autoincrement: If true, the primary key is autoincremented. + /// - returns: Self so that you can further refine the column definition. + @discardableResult + public func primaryKey(onConflict conflictResolution: Database.ConflictResolution? = nil, autoincrement: Bool = false) -> Self { + primaryKey = (conflictResolution: conflictResolution, autoincrement: autoincrement) + return self + } + + /// Adds a NOT NULL constraint on the column. + /// + /// try db.create(table: "players") { t in + /// t.column("name", .text).notNull() + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html#notnullconst + /// + /// - parameter conflitResolution: An optional conflict resolution + /// (see https://www.sqlite.org/lang_conflict.html). + /// - returns: Self so that you can further refine the column definition. + @discardableResult + public func notNull(onConflict conflictResolution: Database.ConflictResolution? = nil) -> Self { + notNullConflictResolution = conflictResolution ?? .abort + return self + } + + /// Adds a UNIQUE constraint on the column. + /// + /// try db.create(table: "players") { t in + /// t.column("email", .text).unique() + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html#uniqueconst + /// + /// - parameter conflitResolution: An optional conflict resolution + /// (see https://www.sqlite.org/lang_conflict.html). + /// - returns: Self so that you can further refine the column definition. + @discardableResult + public func unique(onConflict conflictResolution: Database.ConflictResolution? = nil) -> Self { + index = .unique(conflictResolution ?? .abort) + return self + } + + /// Adds an index of the column. + /// + /// try db.create(table: "players") { t in + /// t.column("email", .text).indexed() + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html#uniqueconst + /// + /// - returns: Self so that you can further refine the column definition. + @discardableResult + public func indexed() -> Self { + if case .none = index { + self.index = .index + } + return self + } + + /// Adds a CHECK constraint on the column. + /// + /// try db.create(table: "players") { t in + /// t.column("name", .text).check { length($0) > 0 } + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html#ckconst + /// + /// - parameter condition: A closure whose argument is an Column that + /// represents the defined column, and returns the expression to check. + /// - returns: Self so that you can further refine the column definition. + @discardableResult + public func check(_ condition: (Column) -> SQLExpressible) -> Self { + checkConstraints.append(condition(Column(name)).sqlExpression) + return self + } + + /// Adds a CHECK constraint on the column. + /// + /// try db.create(table: "players") { t in + /// t.column("name", .text).check(sql: "LENGTH(name) > 0") + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html#ckconst + /// + /// - parameter sql: An SQL snippet. + /// - returns: Self so that you can further refine the column definition. + @discardableResult + public func check(sql: String) -> Self { + checkConstraints.append(SQLExpressionLiteral(sql)) + return self + } + + /// Defines the default column value. + /// + /// try db.create(table: "players") { t in + /// t.column("name", .text).defaults(to: "Anonymous") + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html#dfltval + /// + /// - parameter value: A DatabaseValueConvertible value. + /// - returns: Self so that you can further refine the column definition. + @discardableResult + public func defaults(to value: DatabaseValueConvertible) -> Self { + defaultExpression = value.sqlExpression + return self + } + + /// Defines the default column value. + /// + /// try db.create(table: "players") { t in + /// t.column("creationDate", .DateTime).defaults(sql: "CURRENT_TIMESTAMP") + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html#dfltval + /// + /// - parameter sql: An SQL snippet. + /// - returns: Self so that you can further refine the column definition. + @discardableResult + public func defaults(sql: String) -> Self { + defaultExpression = SQLExpressionLiteral(sql) + return self + } + + // Defines the default column collation. + /// + /// try db.create(table: "players") { t in + /// t.column("email", .text).collate(.nocase) + /// } + /// + /// See https://www.sqlite.org/datatype3.html#collation + /// + /// - parameter collation: An Database.CollationName. + /// - returns: Self so that you can further refine the column definition. + @discardableResult + public func collate(_ collation: Database.CollationName) -> Self { + collationName = collation.rawValue + return self + } + + // Defines the default column collation. + /// + /// try db.create(table: "players") { t in + /// t.column("name", .text).collate(.localizedCaseInsensitiveCompare) + /// } + /// + /// See https://www.sqlite.org/datatype3.html#collation + /// + /// - parameter collation: A custom DatabaseCollation. + /// - returns: Self so that you can further refine the column definition. + @discardableResult + public func collate(_ collation: DatabaseCollation) -> Self { + collationName = collation.name + return self + } + + /// Defines a foreign key. + /// + /// try db.create(table: "books") { t in + /// t.column("authorId", .integer).references("authors", onDelete: .cascade) + /// } + /// + /// See https://www.sqlite.org/foreignkeys.html + /// + /// - parameters + /// - table: The referenced table. + /// - column: The column in the referenced table. If not specified, the + /// column of the primary key of the referenced table is used. + /// - deleteAction: Optional action when the referenced row is deleted. + /// - updateAction: Optional action when the referenced row is updated. + /// - deferred: If true, defines a deferred foreign key constraint. + /// See https://www.sqlite.org/foreignkeys.html#fk_deferred. + /// - returns: Self so that you can further refine the column definition. + @discardableResult + public func references(_ table: String, column: String? = nil, onDelete deleteAction: Database.ForeignKeyAction? = nil, onUpdate updateAction: Database.ForeignKeyAction? = nil, deferred: Bool = false) -> Self { + foreignKeyConstraints.append((table: table, column: column, deleteAction: deleteAction, updateAction: updateAction, deferred: deferred)) + return self + } + + fileprivate func sql(_ db: Database, tableName: String, primaryKeyColumns: [String]?) throws -> String { + var chunks: [String] = [] + chunks.append(name.quotedDatabaseIdentifier) + if let type = type { + chunks.append(type.rawValue) + } + + if let (conflictResolution, autoincrement) = primaryKey { + chunks.append("PRIMARY KEY") + if let conflictResolution = conflictResolution { + chunks.append("ON CONFLICT") + chunks.append(conflictResolution.rawValue) + } + if autoincrement { + chunks.append("AUTOINCREMENT") + } + } + + switch notNullConflictResolution { + case .none: + break + case .abort?: + chunks.append("NOT NULL") + case let conflictResolution?: + chunks.append("NOT NULL ON CONFLICT") + chunks.append(conflictResolution.rawValue) + } + + switch index { + case .none: + break + case .unique(let conflictResolution): + switch conflictResolution { + case .abort: + chunks.append("UNIQUE") + default: + chunks.append("UNIQUE ON CONFLICT") + chunks.append(conflictResolution.rawValue) + } + case .index: + break + } + + for checkConstraint in checkConstraints { + chunks.append("CHECK") + chunks.append("(" + checkConstraint.sql + ")") + } + + if let defaultExpression = defaultExpression { + chunks.append("DEFAULT") + chunks.append(defaultExpression.sql) + } + + if let collationName = collationName { + chunks.append("COLLATE") + chunks.append(collationName) + } + + for (table, column, deleteAction, updateAction, deferred) in foreignKeyConstraints { + chunks.append("REFERENCES") + if let column = column { + // explicit reference + chunks.append("\(table.quotedDatabaseIdentifier)(\(column.quotedDatabaseIdentifier))") + } else if table.lowercased() == tableName.lowercased() { + // implicit autoreference + let primaryKeyColumns = try primaryKeyColumns ?? db.primaryKey(table).columns + chunks.append("\(table.quotedDatabaseIdentifier)(\((primaryKeyColumns.map { $0.quotedDatabaseIdentifier } as [String]).joined(separator: ", ")))") + } else { + // implicit external reference + let primaryKeyColumns = try db.primaryKey(table).columns + chunks.append("\(table.quotedDatabaseIdentifier)(\((primaryKeyColumns.map { $0.quotedDatabaseIdentifier } as [String]).joined(separator: ", ")))") + } + if let deleteAction = deleteAction { + chunks.append("ON DELETE") + chunks.append(deleteAction.rawValue) + } + if let updateAction = updateAction { + chunks.append("ON UPDATE") + chunks.append(updateAction.rawValue) + } + if deferred { + chunks.append("DEFERRABLE INITIALLY DEFERRED") + } + } + + return chunks.joined(separator: " ") + } + + fileprivate func indexDefinition(in table: String) -> IndexDefinition? { + switch index { + case .none: return nil + case .unique: return nil + case .index: + return IndexDefinition( + name: "\(table)_on_\(name)", + table: table, + columns: [name], + unique: false, + ifNotExists: false, + condition: nil) + } + } +} + +private struct IndexDefinition { + let name: String + let table: String + let columns: [String] + let unique: Bool + let ifNotExists: Bool + let condition: SQLExpression? + + func sql() -> String { + var chunks: [String] = [] + chunks.append("CREATE") + if unique { + chunks.append("UNIQUE") + } + chunks.append("INDEX") + if ifNotExists { + chunks.append("IF NOT EXISTS") + } + chunks.append(name.quotedDatabaseIdentifier) + chunks.append("ON") + chunks.append("\(table.quotedDatabaseIdentifier)(\((columns.map { $0.quotedDatabaseIdentifier } as [String]).joined(separator: ", ")))") + if let condition = condition { + chunks.append("WHERE") + chunks.append(condition.sql) + } + return chunks.joined(separator: " ") + } +} diff --git a/Pods/GRDB.swift/GRDB/QueryInterface/VirtualTableModule.swift b/Pods/GRDB.swift/GRDB/QueryInterface/VirtualTableModule.swift new file mode 100644 index 0000000..296d4b3 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/QueryInterface/VirtualTableModule.swift @@ -0,0 +1,124 @@ +/// The protocol for SQLite virtual table modules. It lets you define a DSL for +/// the `Database.create(virtualTable:using:)` method: +/// +/// let module = ... +/// try db.create(virtualTable: "items", using: module) { t in +/// ... +/// } +/// +/// GRDB ships with three concrete classes that implement this protocol: FTS3, +/// FTS4 and FTS5. +public protocol VirtualTableModule { + + /// The type of the closure argument in the + /// `Database.create(virtualTable:using:)` method: + /// + /// try db.create(virtualTable: "items", using: module) { t in + /// // t is TableDefinition + /// } + associatedtype TableDefinition + + /// The name of the module. + var moduleName: String { get } + + /// Returns a table definition that is passed as the closure argument in the + /// `Database.create(virtualTable:using:)` method: + /// + /// try db.create(virtualTable: "items", using: module) { t in + /// // t is the result of makeTableDefinition() + /// } + func makeTableDefinition() -> TableDefinition + + /// Returns the module arguments for the `CREATE VIRTUAL TABLE` query. + func moduleArguments(for definition: TableDefinition, in db: Database) throws -> [String] + + /// Execute any relevant database statement after the virtual table has + /// been created. + func database(_ db: Database, didCreate tableName: String, using definition: TableDefinition) throws +} + +extension Database { + + // MARK: - Database Schema: Virtual Table + + /// Creates a virtual database table. + /// + /// try db.create(virtualTable: "vocabulary", using: "spellfix1") + /// + /// See https://www.sqlite.org/lang_createtable.html + /// + /// - parameters: + /// - name: The table name. + /// - ifNotExists: If false, no error is thrown if table already exists. + /// - module: The name of an SQLite virtual table module. + /// - throws: A DatabaseError whenever an SQLite error occurs. + public func create(virtualTable name: String, ifNotExists: Bool = false, using module: String) throws { + var chunks: [String] = [] + chunks.append("CREATE VIRTUAL TABLE") + if ifNotExists { + chunks.append("IF NOT EXISTS") + } + chunks.append(name.quotedDatabaseIdentifier) + chunks.append("USING") + chunks.append(module) + let sql = chunks.joined(separator: " ") + try execute(sql) + } + + /// Creates a virtual database table. + /// + /// let module = ... + /// try db.create(virtualTable: "pointOfInterests", using: module) { t in + /// ... + /// } + /// + /// The type of the closure argument `t` depends on the type of the module + /// argument: refer to this module's documentation. + /// + /// Use this method to create full-text tables using the FTS3, FTS4, or + /// FTS5 modules: + /// + /// try db.create(virtualTable: "books", using: FTS4()) { t in + /// t.column("title") + /// t.column("author") + /// t.column("body") + /// } + /// + /// See https://www.sqlite.org/lang_createtable.html + /// + /// - parameters: + /// - name: The table name. + /// - ifNotExists: If false, no error is thrown if table already exists. + /// - module: a VirtualTableModule + /// - body: An optional closure that defines the virtual table. + /// - throws: A DatabaseError whenever an SQLite error occurs. + public func create<Module: VirtualTableModule>(virtualTable tableName: String, ifNotExists: Bool = false, using module: Module, _ body: ((Module.TableDefinition) -> Void)? = nil) throws { + // Define virtual table + let definition = module.makeTableDefinition() + if let body = body { + body(definition) + } + + // Create virtual table + var chunks: [String] = [] + chunks.append("CREATE VIRTUAL TABLE") + if ifNotExists { + chunks.append("IF NOT EXISTS") + } + chunks.append(tableName.quotedDatabaseIdentifier) + chunks.append("USING") + let arguments = try module.moduleArguments(for: definition, in: self) + if arguments.isEmpty { + chunks.append(module.moduleName) + } else { + chunks.append(module.moduleName + "(" + arguments.joined(separator: ", ") + ")") + } + let sql = chunks.joined(separator: " ") + + try inSavepoint { + try execute(sql) + try module.database(self, didCreate: tableName, using: definition) + return .commit + } + } +} diff --git a/Pods/GRDB.swift/GRDB/Record/FetchedRecordsController.swift b/Pods/GRDB.swift/GRDB/Record/FetchedRecordsController.swift new file mode 100644 index 0000000..8755c6e --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Record/FetchedRecordsController.swift @@ -0,0 +1,1065 @@ +import Foundation + +#if os(iOS) + import UIKit +#endif + +/// You use FetchedRecordsController to track changes in the results of an +/// SQLite request. +/// +/// See https://github.com/groue/GRDB.swift#fetchedrecordscontroller for +/// more information. +public final class FetchedRecordsController<Record: RowConvertible> { + + // MARK: - Initialization + + /// Creates a fetched records controller initialized from a SQL query and + /// its eventual arguments. + /// + /// let controller = FetchedRecordsController<Wine>( + /// dbQueue, + /// sql: "SELECT * FROM wines WHERE color = ? ORDER BY name", + /// arguments: [Color.red], + /// isSameRecord: { (wine1, wine2) in wine1.id == wine2.id }) + /// + /// - parameters: + /// - databaseWriter: A DatabaseWriter (DatabaseQueue, or DatabasePool) + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - queue: A serial dispatch queue (defaults to the main queue) + /// + /// The fetched records controller tracking callbacks will be + /// notified of changes in this queue. The controller itself must be + /// used from this queue. + /// + /// - isSameRecord: Optional function that compares two records. + /// + /// This function should return true if the two records have the + /// same identity. For example, they have the same id. + public convenience init( + _ databaseWriter: DatabaseWriter, + sql: String, + arguments: StatementArguments? = nil, + adapter: RowAdapter? = nil, + queue: DispatchQueue = .main, + isSameRecord: ((Record, Record) -> Bool)? = nil) throws + { + try self.init( + databaseWriter, + request: SQLRequest(sql, arguments: arguments, adapter: adapter).asRequest(of: Record.self), + queue: queue, + isSameRecord: isSameRecord) + } + + /// Creates a fetched records controller initialized from a fetch request + /// from the [Query Interface](https://github.com/groue/GRDB.swift#the-query-interface). + /// + /// let request = Wine.order(Column("name")) + /// let controller = FetchedRecordsController( + /// dbQueue, + /// request: request, + /// isSameRecord: { (wine1, wine2) in wine1.id == wine2.id }) + /// + /// - parameters: + /// - databaseWriter: A DatabaseWriter (DatabaseQueue, or DatabasePool) + /// - request: A fetch request. + /// - queue: A serial dispatch queue (defaults to the main queue) + /// + /// The fetched records controller tracking callbacks will be + /// notified of changes in this queue. The controller itself must be + /// used from this queue. + /// + /// - isSameRecord: Optional function that compares two records. + /// + /// This function should return true if the two records have the + /// same identity. For example, they have the same id. + public convenience init<Request>( + _ databaseWriter: DatabaseWriter, + request: Request, + queue: DispatchQueue = .main, + isSameRecord: ((Record, Record) -> Bool)? = nil) throws + where Request: TypedRequest, Request.RowDecoder == Record + { + let itemsAreIdenticalFactory: ItemComparatorFactory<Record> + if let isSameRecord = isSameRecord { + itemsAreIdenticalFactory = { _ in { isSameRecord($0.record, $1.record) } } + } else { + itemsAreIdenticalFactory = { _ in { _,_ in false } } + } + + try self.init( + databaseWriter, + request: request, + queue: queue, + itemsAreIdenticalFactory: itemsAreIdenticalFactory) + } + + private init<Request>( + _ databaseWriter: DatabaseWriter, + request: Request, + queue: DispatchQueue, + itemsAreIdenticalFactory: @escaping ItemComparatorFactory<Record>) throws + where Request: TypedRequest, Request.RowDecoder == Record + { + self.itemsAreIdenticalFactory = itemsAreIdenticalFactory + self.request = request + (self.selectionInfo, self.itemsAreIdentical) = try databaseWriter.unsafeRead { db in + try FetchedRecordsController.fetchSelectionInfoAndComparator(db, request: request, itemsAreIdenticalFactory: itemsAreIdenticalFactory) + } + self.databaseWriter = databaseWriter + self.queue = queue + } + + /// Executes the controller's fetch request. + /// + /// After executing this method, you can access the the fetched objects with + /// the `fetchedRecords` property. + /// + /// This method must be used from the controller's dispatch queue (the + /// main queue unless stated otherwise in the controller's initializer). + public func performFetch() throws { + // If some changes are currently processed, make sure they are + // discarded. But preserve eventual changes processing for future + // changes. + let fetchAndNotifyChanges = observer?.fetchAndNotifyChanges + observer?.invalidate() + observer = nil + + // Fetch items on the writing dispatch queue, so that the transaction + // observer is added on the same serialized queue as transaction + // callbacks. + try databaseWriter.write { db in + let initialItems = try Item<Record>.fetchAll(db, request) + fetchedItems = initialItems + if let fetchAndNotifyChanges = fetchAndNotifyChanges { + let observer = FetchedRecordsObserver(selectionInfo: self.selectionInfo, fetchAndNotifyChanges: fetchAndNotifyChanges) + self.observer = observer + observer.items = initialItems + db.add(transactionObserver: observer) + } + } + } + + + // MARK: - Configuration + + /// The database writer used to fetch records. + /// + /// The controller registers as a transaction observer in order to respond + /// to changes. + public let databaseWriter: DatabaseWriter + + /// The dispatch queue on which the controller must be used. + /// + /// Unless specified otherwise at initialization time, it is the main queue. + public let queue: DispatchQueue + + /// Updates the fetch request, and eventually notifies the tracking + /// callbacks if performFetch() has been called. + /// + /// This method must be used from the controller's dispatch queue (the + /// main queue unless stated otherwise in the controller's initializer). + public func setRequest<Request>(_ request: Request) throws where Request: TypedRequest, Request.RowDecoder == Record { + self.request = request + (self.selectionInfo, self.itemsAreIdentical) = try databaseWriter.unsafeRead { db in + try FetchedRecordsController.fetchSelectionInfoAndComparator(db, request: request, itemsAreIdenticalFactory: itemsAreIdenticalFactory) + } + + // No observer: don't look for changes + guard let observer = observer else { return } + + // If some changes are currently processed, make sure they are + // discarded. But preserve eventual changes processing. + let fetchAndNotifyChanges = observer.fetchAndNotifyChanges + observer.invalidate() + self.observer = nil + + // Replace observer so that it tracks a new set of columns, + // and notify eventual changes + let initialItems = fetchedItems + databaseWriter.write { db in + let observer = FetchedRecordsObserver(selectionInfo: selectionInfo, fetchAndNotifyChanges: fetchAndNotifyChanges) + self.observer = observer + observer.items = initialItems + db.add(transactionObserver: observer) + observer.fetchAndNotifyChanges(observer) + } + } + + /// Updates the fetch request, and eventually notifies the tracking + /// callbacks if performFetch() has been called. + /// + /// This method must be used from the controller's dispatch queue (the + /// main queue unless stated otherwise in the controller's initializer). + public func setRequest(sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws { + try setRequest(SQLRequest(sql, arguments: arguments, adapter: adapter).asRequest(of: Record.self)) + } + + /// Registers changes notification callbacks. + /// + /// This method must be used from the controller's dispatch queue (the + /// main queue unless stated otherwise in the controller's initializer). + /// + /// - parameters: + /// - willChange: Invoked before records are updated. + /// - onChange: Invoked for each record that has been added, + /// removed, moved, or updated. + /// - didChange: Invoked after records have been updated. + public func trackChanges( + willChange: ((FetchedRecordsController<Record>) -> ())? = nil, + onChange: ((FetchedRecordsController<Record>, Record, FetchedRecordChange) -> ())? = nil, + didChange: ((FetchedRecordsController<Record>) -> ())? = nil) + { + // I hate you SE-0110. + let wrappedWillChange: ((FetchedRecordsController<Record>, Void) -> ())? + if let willChange = willChange { + wrappedWillChange = { (controller, _) in willChange(controller) } + } else { + wrappedWillChange = nil + } + + let wrappedDidChange: ((FetchedRecordsController<Record>, Void) -> ())? + if let didChange = didChange { + wrappedDidChange = { (controller, _) in didChange(controller) } + } else { + wrappedDidChange = nil + } + + trackChanges( + fetchAlongside: { _ in }, + willChange: wrappedWillChange, + onChange: onChange, + didChange: wrappedDidChange) + + // Without bloody SE-0110: +// trackChanges( +// fetchAlongside: { _ in }, +// willChange: willChange.map { callback in { (controller, _) in callback(controller) } }, +// onChange: onChange, +// didChange: didChange.map { callback in { (controller, _) in callback(controller) } }) + } + + /// Registers changes notification callbacks. + /// + /// This method must be used from the controller's dispatch queue (the + /// main queue unless stated otherwise in the controller's initializer). + /// + /// - parameters: + /// - fetchAlongside: The value returned from this closure is given to + /// willChange and didChange callbacks, as their + /// `fetchedAlongside` argument. The closure is guaranteed to see the + /// database in the state it has just after eventual changes to the + /// fetched records have been performed. Use it in order to fetch + /// values that must be consistent with the fetched records. + /// - willChange: Invoked before records are updated. + /// - onChange: Invoked for each record that has been added, + /// removed, moved, or updated. + /// - didChange: Invoked after records have been updated. + public func trackChanges<T>( + fetchAlongside: @escaping (Database) throws -> T, + willChange: ((FetchedRecordsController<Record>, _ fetchedAlongside: T) -> ())? = nil, + onChange: ((FetchedRecordsController<Record>, Record, FetchedRecordChange) -> ())? = nil, + didChange: ((FetchedRecordsController<Record>, _ fetchedAlongside: T) -> ())? = nil) + { + // If some changes are currently processed, make sure they are + // discarded because they would trigger previously set callbacks. + observer?.invalidate() + observer = nil + + guard (willChange != nil) || (onChange != nil) || (didChange != nil) else { + // Stop tracking + return + } + + var willProcessTransaction: () -> () = { } + var didProcessTransaction: () -> () = { } + #if os(iOS) + if let application = application { + var backgroundTaskID: UIBackgroundTaskIdentifier! = nil + willProcessTransaction = { + backgroundTaskID = application.beginBackgroundTask { + application.endBackgroundTask(backgroundTaskID) + } + } + didProcessTransaction = { + application.endBackgroundTask(backgroundTaskID) + } + } + #endif + + let initialItems = fetchedItems + databaseWriter.write { db in + let fetchAndNotifyChanges = makeFetchAndNotifyChangesFunction( + controller: self, + fetchAlongside: fetchAlongside, + itemsAreIdentical: itemsAreIdentical, + willProcessTransaction: willProcessTransaction, + willChange: willChange, + onChange: onChange, + didChange: didChange, + didProcessTransaction: didProcessTransaction) + let observer = FetchedRecordsObserver(selectionInfo: selectionInfo, fetchAndNotifyChanges: fetchAndNotifyChanges) + self.observer = observer + if let initialItems = initialItems { + observer.items = initialItems + db.add(transactionObserver: observer) + observer.fetchAndNotifyChanges(observer) + } + } + } + + /// Registers an error callback. + /// + /// Whenever the controller could not look for changes after a transaction + /// has potentially modified the tracked request, this error handler is + /// called. + /// + /// The request observation is not stopped, though: future transactions may + /// successfully be handled, and the notified changes will then be based on + /// the last successful fetch. + /// + /// This method must be used from the controller's dispatch queue (the + /// main queue unless stated otherwise in the controller's initializer). + public func trackErrors(_ errorHandler: @escaping (FetchedRecordsController<Record>, Error) -> ()) { + self.errorHandler = errorHandler + } + + #if os(iOS) + /// Call this method when changes performed while the application is + /// in the background should be processed before the application enters the + /// suspended state. + /// + /// Whenever the tracked request is changed, the fetched records controller + /// sets up a background task using + /// `UIApplication.beginBackgroundTask(expirationHandler:)` which is ended + /// after the `didChange` callback has completed. + public func allowBackgroundChangesTracking(in application: UIApplication) { + self.application = application + } + #endif + + // MARK: - Accessing Records + + /// The fetched records. + /// + /// The value of this property is nil until performFetch() has been called. + /// + /// The records reflect the state of the database after the initial + /// call to performFetch, and after each database transaction that affects + /// the results of the fetch request. + /// + /// This property must be used from the controller's dispatch queue (the + /// main queue unless stated otherwise in the controller's initializer). + public var fetchedRecords: [Record] { + guard let fetchedItems = fetchedItems else { + fatalError("the performFetch() method must be called before accessing fetched records") + } + return fetchedItems.map { $0.record } + } + + + // MARK: - Not public + + #if os(iOS) + /// Support for allowBackgroundChangeTracking(in:) + var application: UIApplication? + #endif + + /// The items + fileprivate var fetchedItems: [Item<Record>]? + + /// The record comparator + private var itemsAreIdentical: ItemComparator<Record> + + /// The record comparator factory (support for request change) + private let itemsAreIdenticalFactory: ItemComparatorFactory<Record> + + /// The request + fileprivate var request: Request + + /// The observed selection info + private var selectionInfo : SelectStatement.SelectionInfo + + /// The eventual current database observer + private var observer: FetchedRecordsObserver<Record>? + + /// The eventual error handler + fileprivate var errorHandler: ((FetchedRecordsController<Record>, Error) -> ())? + + private static func fetchSelectionInfoAndComparator( + _ db: Database, + request: Request, + itemsAreIdenticalFactory: ItemComparatorFactory<Record>) throws + -> (SelectStatement.SelectionInfo, ItemComparator<Record>) + { + let (statement, _) = try request.prepare(db) + let selectionInfo = statement.selectionInfo + let itemsAreIdentical = try itemsAreIdenticalFactory(db) + return (selectionInfo, itemsAreIdentical) + } +} + +extension FetchedRecordsController where Record: TableMapping { + + // MARK: - Initialization + + /// Creates a fetched records controller initialized from a SQL query and + /// its eventual arguments. + /// + /// let controller = FetchedRecordsController<Wine>( + /// dbQueue, + /// sql: "SELECT * FROM wines WHERE color = ? ORDER BY name", + /// arguments: [Color.red]) + /// + /// The records are compared by primary key (single-column primary key, + /// compound primary key, or implicit rowid). For a database table which + /// has an `id` primary key, this initializer is equivalent to: + /// + /// // Assuming the wines table has an `id` primary key: + /// let controller = FetchedRecordsController<Wine>( + /// dbQueue, + /// sql: "SELECT * FROM wines WHERE color = ? ORDER BY name", + /// arguments: [Color.red], + /// isSameRecord: { (wine1, wine2) in wine1.id == wine2.id }) + /// + /// - parameters: + /// - databaseWriter: A DatabaseWriter (DatabaseQueue, or DatabasePool) + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - queue: A serial dispatch queue (defaults to the main queue) + /// + /// The fetched records controller tracking callbacks will be + /// notified of changes in this queue. The controller itself must be + /// used from this queue. + public convenience init( + _ databaseWriter: DatabaseWriter, + sql: String, + arguments: StatementArguments? = nil, + adapter: RowAdapter? = nil, + queue: DispatchQueue = .main) throws + { + try self.init( + databaseWriter, + request: SQLRequest(sql, arguments: arguments, adapter: adapter).asRequest(of: Record.self), + queue: queue) + } + + /// Creates a fetched records controller initialized from a fetch request + /// from the [Query Interface](https://github.com/groue/GRDB.swift#the-query-interface). + /// + /// let request = Wine.order(Column("name")) + /// let controller = FetchedRecordsController( + /// dbQueue, + /// request: request) + /// + /// The records are compared by primary key (single-column primary key, + /// compound primary key, or implicit rowid). For a database table which + /// has an `id` primary key, this initializer is equivalent to: + /// + /// // Assuming the wines table has an `id` primary key: + /// let controller = FetchedRecordsController<Wine>( + /// dbQueue, + /// request: request, + /// isSameRecord: { (wine1, wine2) in wine1.id == wine2.id }) + /// + /// - parameters: + /// - databaseWriter: A DatabaseWriter (DatabaseQueue, or DatabasePool) + /// - request: A fetch request. + /// - queue: A serial dispatch queue (defaults to the main queue) + /// + /// The fetched records controller tracking callbacks will be + /// notified of changes in this queue. The controller itself must be + /// used from this queue. + public convenience init<Request>( + _ databaseWriter: DatabaseWriter, + request: Request, + queue: DispatchQueue = .main) throws + where Request: TypedRequest, Request.RowDecoder == Record + { + // Builds a function that returns true if and only if two items + // have the same primary key and primary keys contain at least one + // non-null value. + let itemsAreIdenticalFactory: ItemComparatorFactory<Record> = { db in + // Extract primary key columns from database table + let columns = try db.primaryKey(Record.databaseTableName).columns + + // Compare primary keys + assert(!columns.isEmpty) + return { (lItem, rItem) in + var notNullValue = false + for column in columns { + let lValue: DatabaseValue = lItem.row[column] + let rValue: DatabaseValue = rItem.row[column] + if lValue != rValue { + // different primary keys + return false + } + if !lValue.isNull || !rValue.isNull { + notNullValue = true + } + } + // identical primary keys iff at least one value is not null + return notNullValue + } + } + try self.init( + databaseWriter, + request: request, + queue: queue, + itemsAreIdenticalFactory: itemsAreIdenticalFactory) + } +} + + +// MARK: - FetchedRecordsObserver + +/// FetchedRecordsController adopts TransactionObserverType so that it can +/// monitor changes to its fetched records. +private final class FetchedRecordsObserver<Record: RowConvertible> : TransactionObserver { + var isValid: Bool + var needsComputeChanges: Bool + var items: [Item<Record>]! // ought to be not nil when observer has started tracking transactions + let queue: DispatchQueue // protects items + let selectionInfo: SelectStatement.SelectionInfo + var fetchAndNotifyChanges: (FetchedRecordsObserver<Record>) -> () + + init(selectionInfo: SelectStatement.SelectionInfo, fetchAndNotifyChanges: @escaping (FetchedRecordsObserver<Record>) -> ()) { + self.isValid = true + self.items = nil + self.needsComputeChanges = false + self.queue = DispatchQueue(label: "GRDB.FetchedRecordsObserver") + self.selectionInfo = selectionInfo + self.fetchAndNotifyChanges = fetchAndNotifyChanges + } + + func invalidate() { + isValid = false + } + + func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { + return eventKind.impacts(selectionInfo) + } + + #if SQLITE_ENABLE_PREUPDATE_HOOK + /// Part of the TransactionObserverType protocol + func databaseWillChange(with event: DatabasePreUpdateEvent) { } + #endif + + /// Part of the TransactionObserverType protocol + func databaseDidChange(with event: DatabaseEvent) { + needsComputeChanges = true + } + + /// Part of the TransactionObserverType protocol + func databaseWillCommit() throws { } + + /// Part of the TransactionObserverType protocol + func databaseDidRollback(_ db: Database) { + needsComputeChanges = false + } + + /// Part of the TransactionObserverType protocol + func databaseDidCommit(_ db: Database) { + // The databaseDidCommit callback is called in the database writer + // dispatch queue, which is serialized: it is guaranteed to process the + // last database transaction. + + // Were observed tables modified? + guard needsComputeChanges else { return } + needsComputeChanges = false + + fetchAndNotifyChanges(self) + } +} + + +// MARK: - Changes + +private func makeFetchFunction<Record, T>( + controller: FetchedRecordsController<Record>, + fetchAlongside: @escaping (Database) throws -> T, + willProcessTransaction: @escaping () -> (), + completion: @escaping (Result<(fetchedItems: [Item<Record>], fetchedAlongside: T, observer: FetchedRecordsObserver<Record>)>) -> () + ) -> (FetchedRecordsObserver<Record>) -> () +{ + // Make sure we keep a weak reference to the fetched records controller, + // so that the user can use unowned references in callbacks: + // + // controller.trackChanges { [unowned self] ... } + // + // Should controller become strong at any point before callbacks are + // called, such unowned reference would have an opportunity to crash. + return { [weak controller] observer in + // Return if observer has been invalidated + guard observer.isValid else { return } + + // Return if fetched records controller has been deallocated + guard let request = controller?.request, let databaseWriter = controller?.databaseWriter else { return } + + willProcessTransaction() + + // Fetch items. + // + // This method is called from the database writer's serialized + // queue, so that we can fetch items before other writes have the + // opportunity to modify the database. + // + // However, we don't have to block the writer queue for all the + // duration of the fetch. We just need to block the writer queue + // until we can perform a fetch in isolation. This is the role of + // the readFromCurrentState method (see below). + // + // However, our fetch will last for an unknown duration. And since + // we release the writer queue early, the next database modification + // will triggers this callback while our fetch is, maybe, still + // running. This next callback will also perform its own fetch, that + // will maybe end before our own fetch. + // + // We have to make sure that our fetch is processed *before* the + // next fetch: let's immediately dispatch the processing task in our + // serialized FIFO queue, but have it wait for our fetch to + // complete, with a semaphore: + let semaphore = DispatchSemaphore(value: 0) + var result: Result<(fetchedItems: [Item<Record>], fetchedAlongside: T)>? = nil + do { + try databaseWriter.readFromCurrentState { db in + result = Result { try ( + fetchedItems: Item<Record>.fetchAll(db, request), + fetchedAlongside: fetchAlongside(db)) } + semaphore.signal() + } + } catch { + result = .failure(error) + semaphore.signal() + } + + // Process the fetched items + + observer.queue.async { [weak observer] in + // Wait for the fetch to complete: + _ = semaphore.wait(timeout: .distantFuture) + + // Return if observer has been invalidated + guard let strongObserver = observer else { return } + guard strongObserver.isValid else { return } + + completion(result!.map { (fetchedItems, fetchedAlongside) in + (fetchedItems: fetchedItems, fetchedAlongside: fetchedAlongside, observer: strongObserver) + }) + } + } +} + +private func makeFetchAndNotifyChangesFunction<Record, T>( + controller: FetchedRecordsController<Record>, + fetchAlongside: @escaping (Database) throws -> T, + itemsAreIdentical: @escaping ItemComparator<Record>, + willProcessTransaction: @escaping () -> (), + willChange: ((FetchedRecordsController<Record>, _ fetchedAlongside: T) -> ())?, + onChange: ((FetchedRecordsController<Record>, Record, FetchedRecordChange) -> ())?, + didChange: ((FetchedRecordsController<Record>, _ fetchedAlongside: T) -> ())?, + didProcessTransaction: @escaping () -> () + ) -> (FetchedRecordsObserver<Record>) -> () +{ + // Make sure we keep a weak reference to the fetched records controller, + // so that the user can use unowned references in callbacks: + // + // controller.trackChanges { [unowned self] ... } + // + // Should controller become strong at any point before callbacks are + // called, such unowned reference would have an opportunity to crash. + return makeFetchFunction(controller: controller, fetchAlongside: fetchAlongside, willProcessTransaction: willProcessTransaction) { [weak controller] result in + // Return if fetched records controller has been deallocated + guard let callbackQueue = controller?.queue else { return } + + switch result { + case .failure(let error): + callbackQueue.async { + // Now we can retain controller + guard let strongController = controller else { return } + strongController.errorHandler?(strongController, error) + didProcessTransaction() + } + + case .success((fetchedItems: let fetchedItems, fetchedAlongside: let fetchedAlongside, observer: let observer)): + // Return if there is no change + let changes: [ItemChange<Record>] + if onChange != nil { + // Compute table view changes + changes = computeChanges(from: observer.items, to: fetchedItems, itemsAreIdentical: itemsAreIdentical) + if changes.isEmpty { return } + } else { + // Don't compute changes: just look for a row difference: + if identicalItemArrays(fetchedItems, observer.items) { return } + changes = [] + } + + // Ready for next check + observer.items = fetchedItems + + callbackQueue.async { [weak observer] in + // Return if observer has been invalidated + guard let strongObserver = observer else { return } + guard strongObserver.isValid else { return } + + // Now we can retain controller + guard let strongController = controller else { return } + + // Notify changes + willChange?(strongController, fetchedAlongside) + strongController.fetchedItems = fetchedItems + if let onChange = onChange { + for change in changes { + onChange(strongController, change.record, change.fetchedRecordChange) + } + } + didChange?(strongController, fetchedAlongside) + didProcessTransaction() + } + } + } +} + +private func computeChanges<Record>(from s: [Item<Record>], to t: [Item<Record>], itemsAreIdentical: ItemComparator<Record>) -> [ItemChange<Record>] { + let m = s.count + let n = t.count + + // Fill first row and column of insertions and deletions. + + var d: [[[ItemChange<Record>]]] = Array(repeating: Array(repeating: [], count: n + 1), count: m + 1) + + var changes = [ItemChange<Record>]() + for (row, item) in s.enumerated() { + let deletion = ItemChange.deletion(item: item, indexPath: IndexPath(indexes: [0, row])) + changes.append(deletion) + d[row + 1][0] = changes + } + + changes.removeAll() + for (col, item) in t.enumerated() { + let insertion = ItemChange.insertion(item: item, indexPath: IndexPath(indexes: [0, col])) + changes.append(insertion) + d[0][col + 1] = changes + } + + if m == 0 || n == 0 { + // Pure deletions or insertions + return d[m][n] + } + + // Fill body of matrix. + for tx in 0..<n { + for sx in 0..<m { + if s[sx] == t[tx] { + d[sx+1][tx+1] = d[sx][tx] // no operation + } else { + var del = d[sx][tx+1] // a deletion + var ins = d[sx+1][tx] // an insertion + var sub = d[sx][tx] // a substitution + + // Record operation. + let minimumCount = min(del.count, ins.count, sub.count) + if del.count == minimumCount { + let deletion = ItemChange.deletion(item: s[sx], indexPath: IndexPath(indexes: [0, sx])) + del.append(deletion) + d[sx+1][tx+1] = del + } else if ins.count == minimumCount { + let insertion = ItemChange.insertion(item: t[tx], indexPath: IndexPath(indexes: [0, tx])) + ins.append(insertion) + d[sx+1][tx+1] = ins + } else { + let deletion = ItemChange.deletion(item: s[sx], indexPath: IndexPath(indexes: [0, sx])) + let insertion = ItemChange.insertion(item: t[tx], indexPath: IndexPath(indexes: [0, tx])) + sub.append(deletion) + sub.append(insertion) + d[sx+1][tx+1] = sub + } + } + } + } + + /// Returns an array where deletion/insertion pairs of the same element are replaced by `.move` change. + func standardize(changes: [ItemChange<Record>], itemsAreIdentical: ItemComparator<Record>) -> [ItemChange<Record>] { + + /// Returns a potential .move or .update if *change* has a matching change in *changes*: + /// If *change* is a deletion or an insertion, and there is a matching inverse + /// insertion/deletion with the same value in *changes*, a corresponding .move or .update is returned. + /// As a convenience, the index of the matched change is returned as well. + func merge(change: ItemChange<Record>, in changes: [ItemChange<Record>], itemsAreIdentical: ItemComparator<Record>) -> (mergedChange: ItemChange<Record>, mergedIndex: Int)? { + + /// Returns the changes between two rows: a dictionary [key: oldValue] + /// Precondition: both rows have the same columns + func changedValues(from oldRow: Row, to newRow: Row) -> [String: DatabaseValue] { + var changedValues: [String: DatabaseValue] = [:] + for (column, newValue) in newRow { + let oldValue: DatabaseValue? = oldRow[column] + if newValue != oldValue { + changedValues[column] = oldValue + } + } + return changedValues + } + + switch change { + case .insertion(let newItem, let newIndexPath): + // Look for a matching deletion + for (index, otherChange) in changes.enumerated() { + guard case .deletion(let oldItem, let oldIndexPath) = otherChange else { continue } + guard itemsAreIdentical(oldItem, newItem) else { continue } + let rowChanges = changedValues(from: oldItem.row, to: newItem.row) + if oldIndexPath == newIndexPath { + return (ItemChange.update(item: newItem, indexPath: oldIndexPath, changes: rowChanges), index) + } else { + return (ItemChange.move(item: newItem, indexPath: oldIndexPath, newIndexPath: newIndexPath, changes: rowChanges), index) + } + } + return nil + + case .deletion(let oldItem, let oldIndexPath): + // Look for a matching insertion + for (index, otherChange) in changes.enumerated() { + guard case .insertion(let newItem, let newIndexPath) = otherChange else { continue } + guard itemsAreIdentical(oldItem, newItem) else { continue } + let rowChanges = changedValues(from: oldItem.row, to: newItem.row) + if oldIndexPath == newIndexPath { + return (ItemChange.update(item: newItem, indexPath: oldIndexPath, changes: rowChanges), index) + } else { + return (ItemChange.move(item: newItem, indexPath: oldIndexPath, newIndexPath: newIndexPath, changes: rowChanges), index) + } + } + return nil + + default: + return nil + } + } + + // Updates must be pushed at the end + var mergedChanges: [ItemChange<Record>] = [] + var updateChanges: [ItemChange<Record>] = [] + for change in changes { + if let (mergedChange, mergedIndex) = merge(change: change, in: mergedChanges, itemsAreIdentical: itemsAreIdentical) { + mergedChanges.remove(at: mergedIndex) + switch mergedChange { + case .update: + updateChanges.append(mergedChange) + default: + mergedChanges.append(mergedChange) + } + } else { + mergedChanges.append(change) + } + } + return mergedChanges + updateChanges + } + + return standardize(changes: d[m][n], itemsAreIdentical: itemsAreIdentical) +} + +private func identicalItemArrays<Record>(_ lhs: [Item<Record>], _ rhs: [Item<Record>]) -> Bool { + guard lhs.count == rhs.count else { + return false + } + for (lhs, rhs) in zip(lhs, rhs) { + if lhs.row != rhs.row { + return false + } + } + return true +} + + +// MARK: - UITableView Support + +private typealias ItemComparator<Record: RowConvertible> = (Item<Record>, Item<Record>) -> Bool +private typealias ItemComparatorFactory<Record: RowConvertible> = (Database) throws -> ItemComparator<Record> + +extension FetchedRecordsController { + + // MARK: - Accessing Records + + /// Returns the object at the given index path. + /// + /// - parameter indexPath: An index path in the fetched records. + /// + /// If indexPath does not describe a valid index path in the fetched + /// records, a fatal error is raised. + public func record(at indexPath: IndexPath) -> Record { + guard let fetchedItems = fetchedItems else { + // Programmer error + fatalError("performFetch() has not been called.") + } + return fetchedItems[indexPath[1]].record + } + + + // MARK: - Querying Sections Information + + /// The sections for the fetched records. + /// + /// You typically use the sections array when implementing + /// UITableViewDataSource methods, such as `numberOfSectionsInTableView`. + /// + /// The sections array is never empty, even when there are no fetched + /// records. In this case, there is a single empty section. + public var sections: [FetchedRecordsSectionInfo<Record>] { + // We only support a single section so far. + // We also return a single section when there are no fetched + // records, just like NSFetchedResultsController. + return [FetchedRecordsSectionInfo(controller: self)] + } +} + +extension FetchedRecordsController where Record: MutablePersistable { + + /// Returns the indexPath of a given record. + /// + /// - returns: The index path of *record* in the fetched records, or nil + /// if record could not be found. + public func indexPath(for record: Record) -> IndexPath? { + let item = Item<Record>(row: Row(record)) + guard let fetchedItems = fetchedItems, let index = fetchedItems.index(where: { itemsAreIdentical($0, item) }) else { + return nil + } + return IndexPath(indexes: [0, index]) + } +} + +private enum ItemChange<T: RowConvertible> { + case insertion(item: Item<T>, indexPath: IndexPath) + case deletion(item: Item<T>, indexPath: IndexPath) + case move(item: Item<T>, indexPath: IndexPath, newIndexPath: IndexPath, changes: [String: DatabaseValue]) + case update(item: Item<T>, indexPath: IndexPath, changes: [String: DatabaseValue]) +} + +extension ItemChange { + var record: T { + switch self { + case .insertion(item: let item, indexPath: _): + return item.record + case .deletion(item: let item, indexPath: _): + return item.record + case .move(item: let item, indexPath: _, newIndexPath: _, changes: _): + return item.record + case .update(item: let item, indexPath: _, changes: _): + return item.record + } + } + + var fetchedRecordChange: FetchedRecordChange { + switch self { + case .insertion(item: _, indexPath: let indexPath): + return .insertion(indexPath: indexPath) + case .deletion(item: _, indexPath: let indexPath): + return .deletion(indexPath: indexPath) + case .move(item: _, indexPath: let indexPath, newIndexPath: let newIndexPath, changes: let changes): + return .move(indexPath: indexPath, newIndexPath: newIndexPath, changes: changes) + case .update(item: _, indexPath: let indexPath, changes: let changes): + return .update(indexPath: indexPath, changes: changes) + } + } +} + +extension ItemChange: CustomStringConvertible { + var description: String { + switch self { + case .insertion(let item, let indexPath): + return "Insert \(item) at \(indexPath)" + + case .deletion(let item, let indexPath): + return "Delete \(item) from \(indexPath)" + + case .move(let item, let indexPath, let newIndexPath, changes: let changes): + return "Move \(item) from \(indexPath) to \(newIndexPath) with changes: \(changes)" + + case .update(let item, let indexPath, let changes): + return "Update \(item) at \(indexPath) with changes: \(changes)" + } + } +} + +/// A record change, given by a FetchedRecordsController to its change callback. +/// +/// The move and update events hold a *changes* dictionary, whose keys are +/// column names, and values the old values that have been changed. +public enum FetchedRecordChange { + + /// An insertion event, at given indexPath. + case insertion(indexPath: IndexPath) + + /// A deletion event, at given indexPath. + case deletion(indexPath: IndexPath) + + /// A move event, from indexPath to newIndexPath. The *changes* are a + /// dictionary whose keys are column names, and values the old values that + /// have been changed. + case move(indexPath: IndexPath, newIndexPath: IndexPath, changes: [String: DatabaseValue]) + + /// An update event, at given indexPath. The *changes* are a dictionary + /// whose keys are column names, and values the old values that have + /// been changed. + case update(indexPath: IndexPath, changes: [String: DatabaseValue]) +} + +extension FetchedRecordChange: CustomStringConvertible { + + /// A textual representation of `self`. + public var description: String { + switch self { + case .insertion(let indexPath): + return "Insertion at \(indexPath)" + + case .deletion(let indexPath): + return "Deletion from \(indexPath)" + + case .move(let indexPath, let newIndexPath, changes: let changes): + return "Move from \(indexPath) to \(newIndexPath) with changes: \(changes)" + + case .update(let indexPath, let changes): + return "Update at \(indexPath) with changes: \(changes)" + } + } +} + +/// A section given by a FetchedRecordsController. +public struct FetchedRecordsSectionInfo<Record: RowConvertible> { + fileprivate let controller: FetchedRecordsController<Record> + + /// The number of records (rows) in the section. + public var numberOfRecords: Int { + guard let items = controller.fetchedItems else { + // Programmer error + fatalError("the performFetch() method must be called before accessing section contents") + } + return items.count + } + + /// The array of records in the section. + public var records: [Record] { + guard let items = controller.fetchedItems else { + // Programmer error + fatalError("the performFetch() method must be called before accessing section contents") + } + return items.map { $0.record } + } +} + + +// MARK: - Item + +private final class Item<T: RowConvertible> : RowConvertible, Equatable { + let row: Row + + // Records are lazily loaded + lazy var record: T = T(row: self.row) + + init(row: Row) { + self.row = row.copy() + } +} + +private func ==<T> (lhs: Item<T>, rhs: Item<T>) -> Bool { + return lhs.row == rhs.row +} diff --git a/Pods/GRDB.swift/GRDB/Record/Persistable+Encodable.swift b/Pods/GRDB.swift/GRDB/Record/Persistable+Encodable.swift new file mode 100644 index 0000000..ffcbc50 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Record/Persistable+Encodable.swift @@ -0,0 +1,194 @@ +private struct PersistableKeyedEncodingContainer<Key: CodingKey> : KeyedEncodingContainerProtocol { + let encode: (_ value: DatabaseValueConvertible?, _ key: String) -> Void + + init(encode: @escaping (_ value: DatabaseValueConvertible?, _ key: String) -> Void) { + self.encode = encode + } + + /// The path of coding keys taken to get to this point in encoding. + /// A `nil` value indicates an unkeyed container. + var codingPath: [CodingKey] { return [] } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in the current context for this format. + mutating func encode(_ value: Bool, forKey key: Key) throws { encode(value, key.stringValue) } + mutating func encode(_ value: Int, forKey key: Key) throws { encode(value, key.stringValue) } + mutating func encode(_ value: Int8, forKey key: Key) throws { encode(value, key.stringValue) } + mutating func encode(_ value: Int16, forKey key: Key) throws { encode(value, key.stringValue) } + mutating func encode(_ value: Int32, forKey key: Key) throws { encode(value, key.stringValue) } + mutating func encode(_ value: Int64, forKey key: Key) throws { encode(value, key.stringValue) } + mutating func encode(_ value: UInt, forKey key: Key) throws { encode(value, key.stringValue) } + mutating func encode(_ value: UInt8, forKey key: Key) throws { encode(value, key.stringValue) } + mutating func encode(_ value: UInt16, forKey key: Key) throws { encode(value, key.stringValue) } + mutating func encode(_ value: UInt32, forKey key: Key) throws { encode(value, key.stringValue) } + mutating func encode(_ value: UInt64, forKey key: Key) throws { encode(value, key.stringValue) } + mutating func encode(_ value: Float, forKey key: Key) throws { encode(value, key.stringValue) } + mutating func encode(_ value: Double, forKey key: Key) throws { encode(value, key.stringValue) } + mutating func encode(_ value: String, forKey key: Key) throws { encode(value, key.stringValue) } + mutating func encodeNil(forKey key: Key) throws { encode(nil, key.stringValue) } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in the current context for this format. + mutating func encode<T>(_ value: T, forKey key: Key) throws where T : Encodable { + if T.self is DatabaseValueConvertible.Type { + // Prefer DatabaseValueConvertible encoding over Decodable. + // This allows us to encode Date as String, for example. + encode((value as! DatabaseValueConvertible), key.stringValue) + } else { + try value.encode(to: PersistableEncoder(codingPath: [key], encode: encode)) + } + } + + /// Stores a keyed encoding container for the given key and returns it. + /// + /// - parameter keyType: The key type to use for the container. + /// - parameter key: The key to encode the container for. + /// - returns: A new keyed encoding container. + mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> { + fatalError("Not implemented") + } + + /// Stores an unkeyed encoding container for the given key and returns it. + /// + /// - parameter key: The key to encode the container for. + /// - returns: A new unkeyed encoding container. + mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + fatalError("Not implemented") + } + + /// Stores a new nested container for the default `super` key and returns a new `Encoder` instance for encoding `super` into that container. + /// + /// Equivalent to calling `superEncoder(forKey:)` with `Key(stringValue: "super", intValue: 0)`. + /// + /// - returns: A new `Encoder` to pass to `super.encode(to:)`. + mutating func superEncoder() -> Encoder { + fatalError("Not implemented") + } + + /// Stores a new nested container for the given key and returns a new `Encoder` instance for encoding `super` into that container. + /// + /// - parameter key: The key to encode `super` for. + /// - returns: A new `Encoder` to pass to `super.encode(to:)`. + mutating func superEncoder(forKey key: Key) -> Encoder { + fatalError("Not implemented") + } +} + +private struct DatabaseValueEncodingContainer : SingleValueEncodingContainer { + let key: CodingKey + let encode: (_ value: DatabaseValueConvertible?, _ key: String) -> Void + + var codingPath: [CodingKey] { return [key] } + + /// Encodes a null value. + /// + /// - throws: `EncodingError.invalidValue` if a null value is invalid in the current context for this format. + /// - precondition: May not be called after a previous `self.encode(_:)` call. + func encodeNil() throws { encode(nil, key.stringValue) } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in the current context for this format. + /// - precondition: May not be called after a previous `self.encode(_:)` call. + func encode(_ value: Bool) throws { encode(value, key.stringValue) } + func encode(_ value: Int) throws { encode(value, key.stringValue) } + func encode(_ value: Int8) throws { encode(value, key.stringValue) } + func encode(_ value: Int16) throws { encode(value, key.stringValue) } + func encode(_ value: Int32) throws { encode(value, key.stringValue) } + func encode(_ value: Int64) throws { encode(value, key.stringValue) } + func encode(_ value: UInt) throws { encode(value, key.stringValue) } + func encode(_ value: UInt8) throws { encode(value, key.stringValue) } + func encode(_ value: UInt16) throws { encode(value, key.stringValue) } + func encode(_ value: UInt32) throws { encode(value, key.stringValue) } + func encode(_ value: UInt64) throws { encode(value, key.stringValue) } + func encode(_ value: Float) throws { encode(value, key.stringValue) } + func encode(_ value: Double) throws { encode(value, key.stringValue) } + func encode(_ value: String) throws { encode(value, key.stringValue) } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in the current context for this format. + /// - precondition: May not be called after a previous `self.encode(_:)` call. + func encode<T>(_ value: T) throws where T : Encodable { + if let dbValueConvertible = value as? DatabaseValueConvertible { + // Prefer DatabaseValueConvertible encoding over Decodable. + // This allows us to encode Date as String, for example. + encode(dbValueConvertible.databaseValue, key.stringValue) + } else { + try value.encode(to: PersistableEncoder(codingPath: [key], encode: encode)) + } + } +} + +private struct PersistableEncoder : Encoder { + /// The path of coding keys taken to get to this point in encoding. + /// A `nil` value indicates an unkeyed container. + var codingPath: [CodingKey] + + /// Any contextual information set by the user for encoding. + var userInfo: [CodingUserInfoKey : Any] = [:] + + let encode: (_ value: DatabaseValueConvertible?, _ key: String) -> Void + + init(codingPath: [CodingKey], encode: @escaping (_ value: DatabaseValueConvertible?, _ key: String) -> Void) { + self.codingPath = codingPath + self.encode = encode + } + + /// Returns an encoding container appropriate for holding multiple values keyed by the given key type. + /// + /// - parameter type: The key type to use for the container. + /// - returns: A new keyed encoding container. + /// - precondition: May not be called after a prior `self.unkeyedContainer()` call. + /// - precondition: May not be called after a value has been encoded through a previous `self.singleValueContainer()` call. + func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> { + // Asked for a keyed type: top level required + guard codingPath.isEmpty else { + fatalError("unkeyed encoding is not supported") + } + return KeyedEncodingContainer(PersistableKeyedEncodingContainer<Key>(encode: encode)) + } + + /// Returns an encoding container appropriate for holding multiple unkeyed values. + /// + /// - returns: A new empty unkeyed container. + /// - precondition: May not be called after a prior `self.container(keyedBy:)` call. + /// - precondition: May not be called after a value has been encoded through a previous `self.singleValueContainer()` call. + func unkeyedContainer() -> UnkeyedEncodingContainer { + fatalError("unkeyed encoding is not supported") + } + + /// Returns an encoding container appropriate for holding a single primitive value. + /// + /// - returns: A new empty single value container. + /// - precondition: May not be called after a prior `self.container(keyedBy:)` call. + /// - precondition: May not be called after a prior `self.unkeyedContainer()` call. + /// - precondition: May not be called after a value has been encoded through a previous `self.singleValueContainer()` call. + func singleValueContainer() -> SingleValueEncodingContainer { + return DatabaseValueEncodingContainer(key: codingPath.last!, encode: encode) + } +} + +extension MutablePersistable where Self: Encodable { + public func encode(to container: inout PersistenceContainer) { + // The inout container parameter won't enter an escaping closure since + // SE-0035: https://github.com/apple/swift-evolution/blob/master/proposals/0035-limit-inout-capture.md + // + // So let's use it in a non-escaping closure: + func encode(_ encode: (_ value: DatabaseValueConvertible?, _ key: String) -> Void) { + withoutActuallyEscaping(encode) { escapableEncode in + let encoder = PersistableEncoder(codingPath: [], encode: escapableEncode) + try! self.encode(to: encoder) + } + } + encode { (value, key) in container[key] = value } + } +} diff --git a/Pods/GRDB.swift/GRDB/Record/Persistable.swift b/Pods/GRDB.swift/GRDB/Record/Persistable.swift new file mode 100644 index 0000000..e2f3efa --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Record/Persistable.swift @@ -0,0 +1,1021 @@ +extension Database.ConflictResolution { + var invalidatesLastInsertedRowID: Bool { + switch self { + case .abort, .fail, .rollback, .replace: + return false + case .ignore: + // Statement may have succeeded without inserting any row + return true + } + } +} + +// MARK: - PersistenceError + +/// An error thrown by a type that adopts Persistable. +public enum PersistenceError: Error { + + /// Thrown by MutablePersistable.update() when no matching row could be + /// found in the database. + case recordNotFound(MutablePersistable) +} + +extension PersistenceError : CustomStringConvertible { + /// A textual representation of `self`. + public var description: String { + switch self { + case .recordNotFound(let persistable): + return "Not found: \(persistable)" + } + } +} + +// MARK: - PersistenceContainer + +/// Use persistence containers in the `encode(to:)` method of your +/// persistable records: +/// +/// struct Player : MutablePersistable { +/// var id: Int64? +/// var name: String? +/// +/// func encode(to container: inout PersistenceContainer) { +/// container["id"] = id +/// container["name"] = name +/// } +/// } +public struct PersistenceContainer { + // fileprivate for Row(_:PersistenceContainer) + fileprivate var storage: [String: DatabaseValueConvertible?] + + /// Accesses the value associated with the given column. + /// + /// It is undefined behavior to set different values for the same column. + /// Column names are case insensitive, so defining both "name" and "NAME" + /// is considered undefined behavior. + public subscript(_ column: String) -> DatabaseValueConvertible? { + get { return storage[column] ?? nil } + set { storage.updateValue(newValue, forKey: column) } + } + + /// Accesses the value associated with the given column. + /// + /// It is undefined behavior to set different values for the same column. + /// Column names are case insensitive, so defining both "name" and "NAME" + /// is considered undefined behavior. + public subscript(_ column: Column) -> DatabaseValueConvertible? { + get { return self[column.name] } + set { self[column.name] = newValue } + } + + init() { + storage = [:] + } + + /// Convenience initializer from a record + /// + /// // Sweet + /// let container = PersistenceContainer(record) + /// + /// // Meh + /// var container = PersistenceContainer() + /// record.encode(to: container) + init(_ record: MutablePersistable) { + storage = [:] + record.encode(to: &self) + } + + /// Columns stored in the container, ordered like values. + var columns: [String] { + return Array(storage.keys) + } + + /// Values stored in the container, ordered like columns. + var values: [DatabaseValueConvertible?] { + return Array(storage.values) + } + + /// Accesses the value associated with the given column, in a + /// case-insensitive fashion. + subscript(caseInsensitive column: String) -> DatabaseValueConvertible? { + get { + if let value = storage[column] { + return value + } + let lowercaseColumn = column.lowercased() + for (key, value) in storage where key.lowercased() == lowercaseColumn { + return value + } + return nil + } + set { + if storage[column] != nil { + storage[column] = newValue + return + } + let lowercaseColumn = column.lowercased() + for key in storage.keys where key.lowercased() == lowercaseColumn { + storage[key] = newValue + return + } + + storage[column] = newValue + } + } + + var isEmpty: Bool { + return storage.isEmpty + } + + /// An iterator over the (column, value) pairs + func makeIterator() -> DictionaryIterator<String, DatabaseValueConvertible?> { + return storage.makeIterator() + } +} + +extension Row { + convenience init(_ record: MutablePersistable) { + self.init(PersistenceContainer(record)) + } + + convenience init(_ container: PersistenceContainer) { + self.init(container.storage) + } +} + +// MARK: - MutablePersistable + +/// The MutablePersistable protocol uses this type in order to handle SQLite +/// conflicts when records are inserted or updated. +/// +/// See `MutablePersistable.persistenceConflictPolicy`. +/// +/// See https://www.sqlite.org/lang_conflict.html +public struct PersistenceConflictPolicy { + /// The conflict resolution algorithm for insertions + public let conflictResolutionForInsert: Database.ConflictResolution + + /// The conflict resolution algorithm for updates + public let conflictResolutionForUpdate: Database.ConflictResolution + + /// Creates a policy + public init(insert: Database.ConflictResolution = .abort, update: Database.ConflictResolution = .abort) { + self.conflictResolutionForInsert = insert + self.conflictResolutionForUpdate = update + } +} + +/// Types that adopt MutablePersistable can be inserted, updated, and deleted. +public protocol MutablePersistable : TableMapping { + /// The policy that handles SQLite conflicts when records are inserted + /// or updated. + /// + /// This property is optional: its default value uses the ABORT policy + /// for both insertions and updates, and has GRDB generate regular + /// INSERT and UPDATE queries. + /// + /// If insertions are resolved with .ignore policy, the + /// `didInsert(with:for:)` method is not called upon successful insertion, + /// even if a row was actually inserted without any conflict. + /// + /// See https://www.sqlite.org/lang_conflict.html + static var persistenceConflictPolicy: PersistenceConflictPolicy { get } + + /// Defines the values persisted in the database. + /// + /// Store in the *container* argument all values that should be stored in + /// the columns of the database table (see databaseTableName()). + /// + /// Primary key columns, if any, must be included. + /// + /// struct Player : MutablePersistable { + /// var id: Int64? + /// var name: String? + /// + /// func encode(to container: inout PersistenceContainer) { + /// container["id"] = id + /// container["name"] = name + /// } + /// } + /// + /// It is undefined behavior to set different values for the same column. + /// Column names are case insensitive, so defining both "name" and "NAME" + /// is considered undefined behavior. + func encode(to container: inout PersistenceContainer) + + /// Notifies the record that it was succesfully inserted. + /// + /// Do not call this method directly: it is called for you, in a protected + /// dispatch queue, with the inserted RowID and the eventual + /// INTEGER PRIMARY KEY column name. + /// + /// This method is optional: the default implementation does nothing. + /// + /// struct Player : MutablePersistable { + /// var id: Int64? + /// var name: String? + /// + /// mutating func didInsert(with rowID: Int64, for column: String?) { + /// self.id = rowID + /// } + /// } + /// + /// - parameters: + /// - rowID: The inserted rowID. + /// - column: The name of the eventual INTEGER PRIMARY KEY column. + mutating func didInsert(with rowID: Int64, for column: String?) + + // MARK: - CRUD + + /// Executes an INSERT statement. + /// + /// This method is guaranteed to have inserted a row in the database if it + /// returns without error. + /// + /// Upon successful insertion, the didInsert(with:for:) method + /// is called with the inserted RowID and the eventual INTEGER PRIMARY KEY + /// column name. + /// + /// This method has a default implementation, so your adopting types don't + /// have to implement it. Yet your types can provide their own + /// implementation of insert(). In their implementation, it is recommended + /// that they invoke the performInsert() method. + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError whenever an SQLite error occurs. + mutating func insert(_ db: Database) throws + + /// Executes an UPDATE statement. + /// + /// This method is guaranteed to have updated a row in the database if it + /// returns without error. + /// + /// This method has a default implementation, so your adopting types don't + /// have to implement it. Yet your types can provide their own + /// implementation of update(). In their implementation, it is recommended + /// that they invoke the performUpdate() method. + /// + /// - parameter db: A database connection. + /// - parameter columns: The columns to update. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + /// PersistenceError.recordNotFound is thrown if the primary key does not + /// match any row in the database. + func update(_ db: Database, columns: Set<String>) throws + + /// Executes an INSERT or an UPDATE statement so that `self` is saved in + /// the database. + /// + /// If the receiver has a non-nil primary key and a matching row in the + /// database, this method performs an update. + /// + /// Otherwise, performs an insert. + /// + /// This method is guaranteed to have inserted or updated a row in the + /// database if it returns without error. + /// + /// This method has a default implementation, so your adopting types don't + /// have to implement it. Yet your types can provide their own + /// implementation of save(). In their implementation, it is recommended + /// that they invoke the performSave() method. + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError whenever an SQLite error occurs, or errors + /// thrown by update(). + mutating func save(_ db: Database) throws + + /// Executes a DELETE statement. + /// + /// This method has a default implementation, so your adopting types don't + /// have to implement it. Yet your types can provide their own + /// implementation of delete(). In their implementation, it is recommended + /// that they invoke the performDelete() method. + /// + /// - parameter db: A database connection. + /// - returns: Whether a database row was deleted. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + @discardableResult + func delete(_ db: Database) throws -> Bool + + /// Returns true if and only if the primary key matches a row in + /// the database. + /// + /// This method has a default implementation, so your adopting types don't + /// have to implement it. Yet your types can provide their own + /// implementation of exists(). In their implementation, it is recommended + /// that they invoke the performExists() method. + /// + /// - parameter db: A database connection. + /// - returns: Whether the primary key matches a row in the database. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + func exists(_ db: Database) throws -> Bool +} + +extension MutablePersistable { + /// A dictionary whose keys are the columns encoded in the `encode(to:)` method. + public var databaseDictionary: [String: DatabaseValue] { + return PersistenceContainer(self).storage.mapValues { $0?.databaseValue ?? .null } + } +} + +extension MutablePersistable { + /// Describes the conflict policy for insertions and updates. + /// + /// The default value specifies ABORT policy for both insertions and + /// updates, which has GRDB generate regular INSERT and UPDATE queries. + public static var persistenceConflictPolicy: PersistenceConflictPolicy { + return PersistenceConflictPolicy(insert: .abort, update: .abort) + } + + /// Notifies the record that it was succesfully inserted. + /// + /// The default implementation does nothing. + public mutating func didInsert(with rowID: Int64, for column: String?) { + } + + // MARK: - CRUD + + /// Executes an INSERT statement. + /// + /// The default implementation for insert() invokes performInsert(). + public mutating func insert(_ db: Database) throws { + try performInsert(db) + } + + /// Executes an UPDATE statement. + /// + /// - parameter db: A database connection. + /// - parameter columns: The columns to update. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + /// PersistenceError.recordNotFound is thrown if the primary key does not + /// match any row in the database. + public func update(_ db: Database, columns: Set<String>) throws { + try performUpdate(db, columns: columns) + } + + /// Executes an UPDATE statement. + /// + /// - parameter db: A database connection. + /// - parameter columns: The columns to update. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + /// PersistenceError.recordNotFound is thrown if the primary key does not + /// match any row in the database. + public func update<Sequence: Swift.Sequence>(_ db: Database, columns: Sequence) throws where Sequence.Element == Column { + try update(db, columns: Set(columns.map { $0.name })) + } + + /// Executes an UPDATE statement. + /// + /// - parameter db: A database connection. + /// - parameter columns: The columns to update. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + /// PersistenceError.recordNotFound is thrown if the primary key does not + /// match any row in the database. + public func update<Sequence: Swift.Sequence>(_ db: Database, columns: Sequence) throws where Sequence.Element == String { + try update(db, columns: Set(columns)) + } + + /// Executes an UPDATE statement that updates all table columns. + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + /// PersistenceError.recordNotFound is thrown if the primary key does not + /// match any row in the database. + public func update(_ db: Database) throws { + let databaseTableName = type(of: self).databaseTableName + let columns = try db.columns(in: databaseTableName) + try update(db, columns: Set(columns.map { $0.name })) + } + + /// Executes an INSERT or an UPDATE statement so that `self` is saved in + /// the database. + /// + /// The default implementation for save() invokes performSave(). + public mutating func save(_ db: Database) throws { + try performSave(db) + } + + /// Executes a DELETE statement. + /// + /// The default implementation for delete() invokes performDelete(). + @discardableResult + public func delete(_ db: Database) throws -> Bool { + return try performDelete(db) + } + + /// Returns true if and only if the primary key matches a row in + /// the database. + /// + /// The default implementation for exists() invokes performExists(). + public func exists(_ db: Database) throws -> Bool { + return try performExists(db) + } + + // MARK: - CRUD Internals + + /// Return true if record has a non-null primary key + fileprivate func canUpdate(_ db: Database) throws -> Bool { + let databaseTableName = type(of: self).databaseTableName + let primaryKey = try db.primaryKey(databaseTableName) + let container = PersistenceContainer(self) + for column in primaryKey.columns { + if let value = container[caseInsensitive: column], !value.databaseValue.isNull { + return true + } + } + return false + } + + /// Don't invoke this method directly: it is an internal method for types + /// that adopt MutablePersistable. + /// + /// performInsert() provides the default implementation for insert(). Types + /// that adopt MutablePersistable can invoke performInsert() in their + /// implementation of insert(). They should not provide their own + /// implementation of performInsert(). + public mutating func performInsert(_ db: Database) throws { + let conflictResolutionForInsert = type(of: self).persistenceConflictPolicy.conflictResolutionForInsert + let dao = try DAO(db, self) + try dao.insertStatement(onConflict: conflictResolutionForInsert).execute() + + if !conflictResolutionForInsert.invalidatesLastInsertedRowID { + didInsert(with: db.lastInsertedRowID, for: dao.primaryKey.rowIDColumn) + } + } + + /// Don't invoke this method directly: it is an internal method for types + /// that adopt MutablePersistable. + /// + /// performUpdate() provides the default implementation for update(). Types + /// that adopt MutablePersistable can invoke performUpdate() in their + /// implementation of update(). They should not provide their own + /// implementation of performUpdate(). + /// + /// - parameter db: A database connection. + /// - parameter columns: The columns to update. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + /// PersistenceError.recordNotFound is thrown if the primary key does not + /// match any row in the database. + public func performUpdate(_ db: Database, columns: Set<String>) throws { + guard let statement = try DAO(db, self).updateStatement(columns: columns, onConflict: type(of: self).persistenceConflictPolicy.conflictResolutionForUpdate) else { + // Nil primary key + throw PersistenceError.recordNotFound(self) + } + try statement.execute() + if db.changesCount == 0 { + throw PersistenceError.recordNotFound(self) + } + } + + /// Don't invoke this method directly: it is an internal method for types + /// that adopt MutablePersistable. + /// + /// performSave() provides the default implementation for save(). Types + /// that adopt MutablePersistable can invoke performSave() in their + /// implementation of save(). They should not provide their own + /// implementation of performSave(). + /// + /// This default implementation forwards the job to `update` or `insert`. + public mutating func performSave(_ db: Database) throws { + // Make sure we call self.insert and self.update so that classes + // that override insert or save have opportunity to perform their + // custom job. + + if try canUpdate(db) { + do { + try update(db) + } catch PersistenceError.recordNotFound { + // TODO: check that the not persisted objet is self + // + // Why? Adopting types could override update() and update + // another object which may be the one throwing this error. + try insert(db) + } + } else { + try insert(db) + } + } + + /// Don't invoke this method directly: it is an internal method for types + /// that adopt MutablePersistable. + /// + /// performDelete() provides the default implementation for deelte(). Types + /// that adopt MutablePersistable can invoke performDelete() in + /// their implementation of delete(). They should not provide their own + /// implementation of performDelete(). + public func performDelete(_ db: Database) throws -> Bool { + guard let statement = try DAO(db, self).deleteStatement() else { + // Nil primary key + return false + } + try statement.execute() + return db.changesCount > 0 + } + + /// Don't invoke this method directly: it is an internal method for types + /// that adopt MutablePersistable. + /// + /// performExists() provides the default implementation for exists(). Types + /// that adopt MutablePersistable can invoke performExists() in + /// their implementation of exists(). They should not provide their own + /// implementation of performExists(). + public func performExists(_ db: Database) throws -> Bool { + guard let statement = try DAO(db, self).existsStatement() else { + // Nil primary key + return false + } + return try Row.fetchOne(statement) != nil + } + +} + +extension MutablePersistable { + + // MARK: - Deleting All + + /// Deletes all records; returns the number of deleted rows. + /// + /// - parameter db: A database connection. + /// - returns: The number of deleted rows + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + @discardableResult + public static func deleteAll(_ db: Database) throws -> Int { + return try all().deleteAll(db) + } +} + +extension MutablePersistable { + + // MARK: - Deleting by Single-Column Primary Key + + /// Delete records identified by their primary keys; returns the number of + /// deleted rows. + /// + /// // DELETE FROM players WHERE id IN (1, 2, 3) + /// try Player.deleteAll(db, keys: [1, 2, 3]) + /// + /// // DELETE FROM countries WHERE code IN ('FR', 'US', 'DE') + /// try Country.deleteAll(db, keys: ["FR", "US", "DE"]) + /// + /// When the table has no explicit primary key, GRDB uses the hidden + /// "rowid" column: + /// + /// // DELETE FROM documents WHERE rowid IN (1, 2, 3) + /// try Document.deleteAll(db, keys: [1, 2, 3]) + /// + /// - parameters: + /// - db: A database connection. + /// - keys: A sequence of primary keys. + /// - returns: The number of deleted rows + @discardableResult + public static func deleteAll<Sequence: Swift.Sequence>(_ db: Database, keys: Sequence) throws -> Int where Sequence.Element: DatabaseValueConvertible { + let keys = Array(keys) + if keys.isEmpty { + // Avoid hitting the database + return 0 + } + return try filter(db, keys: keys).deleteAll(db) + } + + /// Delete a record, identified by its primary key; returns whether a + /// database row was deleted. + /// + /// // DELETE FROM players WHERE id = 123 + /// try Player.deleteOne(db, key: 123) + /// + /// // DELETE FROM countries WHERE code = 'FR' + /// try Country.deleteOne(db, key: "FR") + /// + /// When the table has no explicit primary key, GRDB uses the hidden + /// "rowid" column: + /// + /// // DELETE FROM documents WHERE rowid = 1 + /// try Document.deleteOne(db, key: 1) + /// + /// - parameters: + /// - db: A database connection. + /// - key: A primary key value. + /// - returns: Whether a database row was deleted. + @discardableResult + public static func deleteOne<PrimaryKeyType: DatabaseValueConvertible>(_ db: Database, key: PrimaryKeyType?) throws -> Bool { + guard let key = key else { + // Avoid hitting the database + return false + } + return try deleteAll(db, keys: [key]) > 0 + } +} + +extension MutablePersistable { + + // MARK: - Deleting by Key + + /// Delete records identified by the provided unique keys (primary key or + /// any key with a unique index on it); returns the number of deleted rows. + /// + /// try Player.deleteAll(db, keys: [["email": "a@example.com"], ["email": "b@example.com"]]) + /// + /// - parameters: + /// - db: A database connection. + /// - keys: An array of key dictionaries. + /// - returns: The number of deleted rows + @discardableResult + public static func deleteAll(_ db: Database, keys: [[String: DatabaseValueConvertible?]]) throws -> Int { + if keys.isEmpty { + // Avoid hitting the database + return 0 + } + return try filter(db, keys: keys).deleteAll(db) + } + + /// Delete a record, identified by a unique key (the primary key or any key + /// with a unique index on it); returns whether a database row was deleted. + /// + /// Player.deleteOne(db, key: ["name": Arthur"]) + /// + /// - parameters: + /// - db: A database connection. + /// - key: A dictionary of values. + /// - returns: Whether a database row was deleted. + @discardableResult + public static func deleteOne(_ db: Database, key: [String: DatabaseValueConvertible?]) throws -> Bool { + return try deleteAll(db, keys: [key]) > 0 + } +} + +// MARK: - Persistable + +/// Types that adopt Persistable can be inserted, updated, and deleted. +/// +/// This protocol is intented for types that don't have an INTEGER PRIMARY KEY. +/// +/// Unlike MutablePersistable, the insert() and save() methods are not +/// mutating methods. +public protocol Persistable : MutablePersistable { + + /// Notifies the record that it was succesfully inserted. + /// + /// Do not call this method directly: it is called for you, in a protected + /// dispatch queue, with the inserted RowID and the eventual + /// INTEGER PRIMARY KEY column name. + /// + /// This method is optional: the default implementation does nothing. + /// + /// If you need a mutating variant of this method, adopt the + /// MutablePersistable protocol instead. + /// + /// - parameters: + /// - rowID: The inserted rowID. + /// - column: The name of the eventual INTEGER PRIMARY KEY column. + func didInsert(with rowID: Int64, for column: String?) + + /// Executes an INSERT statement. + /// + /// This method is guaranteed to have inserted a row in the database if it + /// returns without error. + /// + /// Upon successful insertion, the didInsert(with:for:) method + /// is called with the inserted RowID and the eventual INTEGER PRIMARY KEY + /// column name. + /// + /// This method has a default implementation, so your adopting types don't + /// have to implement it. Yet your types can provide their own + /// implementation of insert(). In their implementation, it is recommended + /// that they invoke the performInsert() method. + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError whenever an SQLite error occurs. + func insert(_ db: Database) throws + + /// Executes an INSERT or an UPDATE statement so that `self` is saved in + /// the database. + /// + /// If the receiver has a non-nil primary key and a matching row in the + /// database, this method performs an update. + /// + /// Otherwise, performs an insert. + /// + /// This method is guaranteed to have inserted or updated a row in the + /// database if it returns without error. + /// + /// This method has a default implementation, so your adopting types don't + /// have to implement it. Yet your types can provide their own + /// implementation of save(). In their implementation, it is recommended + /// that they invoke the performSave() method. + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError whenever an SQLite error occurs, or errors + /// thrown by update(). + func save(_ db: Database) throws +} + +extension Persistable { + + /// Notifies the record that it was succesfully inserted. + /// + /// The default implementation does nothing. + public func didInsert(with rowID: Int64, for column: String?) { + } + + // MARK: - Immutable CRUD + + /// Executes an INSERT statement. + /// + /// The default implementation for insert() invokes performInsert(). + public func insert(_ db: Database) throws { + try performInsert(db) + } + + /// Executes an INSERT or an UPDATE statement so that `self` is saved in + /// the database. + /// + /// The default implementation for save() invokes performSave(). + public func save(_ db: Database) throws { + try performSave(db) + } + + + // MARK: - Immutable CRUD Internals + + /// Don't invoke this method directly: it is an internal method for types + /// that adopt Persistable. + /// + /// performInsert() provides the default implementation for insert(). Types + /// that adopt Persistable can invoke performInsert() in their + /// implementation of insert(). They should not provide their own + /// implementation of performInsert(). + public func performInsert(_ db: Database) throws { + let conflictResolutionForInsert = type(of: self).persistenceConflictPolicy.conflictResolutionForInsert + let dao = try DAO(db, self) + try dao.insertStatement(onConflict: conflictResolutionForInsert).execute() + + if !conflictResolutionForInsert.invalidatesLastInsertedRowID { + didInsert(with: db.lastInsertedRowID, for: dao.primaryKey.rowIDColumn) + } + } + + /// Don't invoke this method directly: it is an internal method for types + /// that adopt Persistable. + /// + /// performSave() provides the default implementation for save(). Types + /// that adopt Persistable can invoke performSave() in their + /// implementation of save(). They should not provide their own + /// implementation of performSave(). + /// + /// This default implementation forwards the job to `update` or `insert`. + public func performSave(_ db: Database) throws { + // Make sure we call self.insert and self.update so that classes that + // override insert or save have opportunity to perform their custom job. + + if try canUpdate(db) { + do { + try update(db) + } catch PersistenceError.recordNotFound { + // TODO: check that the not persisted objet is self + // + // Why? Adopting types could override update() and update another + // object which may be the one throwing this error. + try insert(db) + } + } else { + try insert(db) + } + } + +} + +// MARK: - DAO + +/// DAO takes care of Persistable CRUD +final class DAO { + + /// The database + let db: Database + + /// The record + let record: MutablePersistable + + /// DAO keeps a copy the record's persistenceContainer, so that this + /// dictionary is built once whatever the database operation. It is + /// guaranteed to have at least one (key, value) pair. + let persistenceContainer: PersistenceContainer + + /// The table name + let databaseTableName: String + + /// The table primary key + let primaryKey: PrimaryKeyInfo + + init(_ db: Database, _ record: MutablePersistable) throws { + let databaseTableName = type(of: record).databaseTableName + let primaryKey = try db.primaryKey(databaseTableName) + let persistenceContainer = PersistenceContainer(record) + + GRDBPrecondition(!persistenceContainer.isEmpty, "\(type(of: record)): invalid empty persistence container") + + self.db = db + self.record = record + self.persistenceContainer = persistenceContainer + self.databaseTableName = databaseTableName + self.primaryKey = primaryKey + } + + func insertStatement(onConflict: Database.ConflictResolution) throws -> UpdateStatement { + let query = InsertQuery( + onConflict: onConflict, + tableName: databaseTableName, + insertedColumns: persistenceContainer.columns) + let statement = try db.updateStatement(query.sql, fromCache: .grdb) + statement.unsafeSetArguments(StatementArguments(persistenceContainer.values)) + return statement + } + + /// Returns nil if and only if primary key is nil + func updateStatement(columns: Set<String>, onConflict: Database.ConflictResolution) throws -> UpdateStatement? { + // Fail early if primary key does not resolve to a database row. + let primaryKeyColumns = primaryKey.columns + let primaryKeyValues = primaryKeyColumns.map { + persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null + } + guard primaryKeyValues.contains(where: { !$0.isNull }) else { return nil } + + let lowercasePersistentColumns = Set(persistenceContainer.columns.map { $0.lowercased() }) + let lowercasePrimaryKeyColumns = Set(primaryKeyColumns.map { $0.lowercased() }) + var updatedColumns: [String] = [] + for column in columns { + let lowercaseColumn = column.lowercased() + // Don't update columns that are not present in the persistenceContainer + guard lowercasePersistentColumns.contains(lowercaseColumn) else { continue } + // Don't update primary key columns + guard !lowercasePrimaryKeyColumns.contains(lowercaseColumn) else { continue } + updatedColumns.append(column) + } + + if updatedColumns.isEmpty { + // IMPLEMENTATION NOTE + // + // It is important to update something, so that + // TransactionObserver can observe a change even though this + // change is useless. + // + // The goal is to be able to write tests with minimal tables, + // including tables made of a single primary key column. + updatedColumns = primaryKeyColumns + } + let updatedValues = updatedColumns.map { + persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null + } + + let query = UpdateQuery( + onConflict: onConflict, + tableName: databaseTableName, + updatedColumns: updatedColumns, + conditionColumns: primaryKeyColumns) + let statement = try db.updateStatement(query.sql, fromCache: .grdb) + statement.unsafeSetArguments(StatementArguments(updatedValues + primaryKeyValues)) + return statement + } + + /// Returns nil if and only if primary key is nil + func deleteStatement() throws -> UpdateStatement? { + // Fail early if primary key does not resolve to a database row. + let primaryKeyColumns = primaryKey.columns + let primaryKeyValues = primaryKeyColumns.map { + persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null + } + guard primaryKeyValues.contains(where: { !$0.isNull }) else { return nil } + + let query = DeleteQuery( + tableName: databaseTableName, + conditionColumns: primaryKeyColumns) + let statement = try db.updateStatement(query.sql, fromCache: .grdb) + statement.unsafeSetArguments(StatementArguments(primaryKeyValues)) + return statement + } + + /// Returns nil if and only if primary key is nil + func existsStatement() throws -> SelectStatement? { + // Fail early if primary key does not resolve to a database row. + let primaryKeyColumns = primaryKey.columns + let primaryKeyValues = primaryKeyColumns.map { + persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null + } + guard primaryKeyValues.contains(where: { !$0.isNull }) else { return nil } + + let query = ExistsQuery( + tableName: databaseTableName, + conditionColumns: primaryKeyColumns) + let statement = try db.selectStatement(query.sql, fromCache: .grdb) + statement.unsafeSetArguments(StatementArguments(primaryKeyValues)) + return statement + } +} + + +// MARK: - InsertQuery + +private struct InsertQuery { + let onConflict: Database.ConflictResolution + let tableName: String + let insertedColumns: [String] +} + +extension InsertQuery : Hashable { + var hashValue: Int { return tableName.hashValue } + + static func == (lhs: InsertQuery, rhs: InsertQuery) -> Bool { + if lhs.tableName != rhs.tableName { return false } + if lhs.onConflict != rhs.onConflict { return false } + return lhs.insertedColumns == rhs.insertedColumns + } +} + +extension InsertQuery { + static let sqlCache = ReadWriteBox([InsertQuery: String]()) + var sql: String { + if let sql = InsertQuery.sqlCache.read({ $0[self] }) { + return sql + } + let columnsSQL = insertedColumns.map { $0.quotedDatabaseIdentifier }.joined(separator: ", ") + let valuesSQL = databaseQuestionMarks(count: insertedColumns.count) + let sql: String + switch onConflict { + case .abort: + sql = "INSERT INTO \(tableName.quotedDatabaseIdentifier) (\(columnsSQL)) VALUES (\(valuesSQL))" + default: + sql = "INSERT OR \(onConflict.rawValue) INTO \(tableName.quotedDatabaseIdentifier) (\(columnsSQL)) VALUES (\(valuesSQL))" + } + InsertQuery.sqlCache.write { $0[self] = sql } + return sql + } +} + + +// MARK: - UpdateQuery + +private struct UpdateQuery { + let onConflict: Database.ConflictResolution + let tableName: String + let updatedColumns: [String] + let conditionColumns: [String] +} + +extension UpdateQuery : Hashable { + var hashValue: Int { return tableName.hashValue } + + static func == (lhs: UpdateQuery, rhs: UpdateQuery) -> Bool { + if lhs.tableName != rhs.tableName { return false } + if lhs.onConflict != rhs.onConflict { return false } + if lhs.updatedColumns != rhs.updatedColumns { return false } + return lhs.conditionColumns == rhs.conditionColumns + } +} + +extension UpdateQuery { + static let sqlCache = ReadWriteBox([UpdateQuery: String]()) + var sql: String { + if let sql = UpdateQuery.sqlCache.read({ $0[self] }) { + return sql + } + let updateSQL = updatedColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: ", ") + let whereSQL = conditionColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: " AND ") + let sql: String + switch onConflict { + case .abort: + sql = "UPDATE \(tableName.quotedDatabaseIdentifier) SET \(updateSQL) WHERE \(whereSQL)" + default: + sql = "UPDATE OR \(onConflict.rawValue) \(tableName.quotedDatabaseIdentifier) SET \(updateSQL) WHERE \(whereSQL)" + } + UpdateQuery.sqlCache.write { $0[self] = sql } + return sql + } +} + + +// MARK: - DeleteQuery + +private struct DeleteQuery { + let tableName: String + let conditionColumns: [String] +} + +extension DeleteQuery { + var sql: String { + let whereSQL = conditionColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: " AND ") + return "DELETE FROM \(tableName.quotedDatabaseIdentifier) WHERE \(whereSQL)" + } +} + + +// MARK: - ExistsQuery + +private struct ExistsQuery { + let tableName: String + let conditionColumns: [String] +} + +extension ExistsQuery { + var sql: String { + let whereSQL = conditionColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: " AND ") + return "SELECT 1 FROM \(tableName.quotedDatabaseIdentifier) WHERE \(whereSQL)" + } +} diff --git a/Pods/GRDB.swift/GRDB/Record/Record.swift b/Pods/GRDB.swift/GRDB/Record/Record.swift new file mode 100644 index 0000000..9d08d12 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Record/Record.swift @@ -0,0 +1,365 @@ +// MARK: - Record + +/// Record is a class that wraps a table row, or the result of any query. It is +/// designed to be subclassed. +open class Record : RowConvertible, TableMapping, Persistable { + + // MARK: - Initializers + + /// Creates a Record. + public init() { + } + + /// Creates a Record from a row. + required public init(row: Row) { + if row.isFetched { + // Take care of the hasPersistentChangedValues flag. + // + // Row may be a reused row which will turn invalid as soon as the + // SQLite statement is iterated. We need to store an + // immutable copy. + referenceRow = row.copy() + } + } + + + // MARK: - Core methods + + /// The name of a database table. + /// + /// This table name is required by the insert, update, save, delete, + /// and exists methods. + /// + /// class Player : Record { + /// override class var databaseTableName: String { + /// return "players" + /// } + /// } + /// + /// The implementation of the base class Record raises a fatal error. + /// + /// - returns: The name of a database table. + open class var databaseTableName: String { + // Programmer error + fatalError("subclass must override") + } + + /// The policy that handles SQLite conflicts when records are inserted + /// or updated. + /// + /// This property is optional: its default value uses the ABORT policy + /// for both insertions and updates, and has GRDB generate regular + /// INSERT and UPDATE queries. + /// + /// If insertions are resolved with .ignore policy, the + /// `didInsert(with:for:)` method is not called upon successful insertion, + /// even if a row was actually inserted without any conflict. + /// + /// See https://www.sqlite.org/lang_conflict.html + open class var persistenceConflictPolicy: PersistenceConflictPolicy { + return PersistenceConflictPolicy(insert: .abort, update: .abort) + } + + /// The default request selection. + /// + /// Unless this method is overriden, requests select all columns: + /// + /// // SELECT * FROM players + /// try Player.fetchAll(db) + /// + /// You can override this property and provide an explicit list + /// of columns: + /// + /// class RestrictedPlayer : Record { + /// override static var databaseSelection: [SQLSelectable] { + /// return [Column("id"), Column("name")] + /// } + /// } + /// + /// // SELECT id, name FROM players + /// try RestrictedPlayer.fetchAll(db) + /// + /// You can also add extra columns such as the `rowid` column: + /// + /// class ExtendedPlayer : Player { + /// override static var databaseSelection: [SQLSelectable] { + /// return [AllColumns(), Column.rowID] + /// } + /// } + /// + /// // SELECT *, rowid FROM players + /// try ExtendedPlayer.fetchAll(db) + open class var databaseSelection: [SQLSelectable] { + return [AllColumns()] + } + + + /// Defines the values persisted in the database. + /// + /// Store in the *container* argument all values that should be stored in + /// the columns of the database table (see Record.databaseTableName()). + /// + /// Primary key columns, if any, must be included. + /// + /// class Player : Record { + /// var id: Int64? + /// var name: String? + /// + /// override func encode(to container: inout PersistenceContainer) { + /// container["id"] = id + /// container["name"] = name + /// } + /// } + /// + /// The implementation of the base class Record does not store any value in + /// the container. + open func encode(to container: inout PersistenceContainer) { + } + + /// Notifies the record that it was succesfully inserted. + /// + /// Do not call this method directly: it is called for you, in a protected + /// dispatch queue, with the inserted RowID and the eventual + /// INTEGER PRIMARY KEY column name. + /// + /// The implementation of the base Record class does nothing. + /// + /// class Player : Record { + /// var id: Int64? + /// var name: String? + /// + /// func didInsert(with rowID: Int64, for column: String?) { + /// id = rowID + /// } + /// } + /// + /// - parameters: + /// - rowID: The inserted rowID. + /// - column: The name of the eventual INTEGER PRIMARY KEY column. + open func didInsert(with rowID: Int64, for column: String?) { + } + + + // MARK: - Copy + + /// Returns a copy of `self`, initialized from all values encoded in the + /// `encode(to:)` method. + /// + /// The eventual primary key is copied, as well as the + /// `hasPersistentChangedValues` flag. + /// + /// - returns: A copy of self. + open func copy() -> Self { + let row: Row + #if swift(>=3.1) + row = Row(self) + #else + // workaround weird Swift 3.0 glitch + row = Row(self as! MutablePersistable) + #endif + let copy = type(of: self).init(row: row) + copy.referenceRow = referenceRow + return copy + } + + + // MARK: - Changes Tracking + + /// A boolean that indicates whether the record has changes that have not + /// been saved. + /// + /// This flag is purely informative, and does not prevent insert(), + /// update(), and save() from performing their database queries. + /// + /// A record is *edited* if has been changed since last database + /// synchronization (fetch, update, insert). Comparison + /// is performed between *values* (values stored in the `encode(to:)` + /// method, and values loaded from the database). Property setters do not + /// trigger this flag. + /// + /// You can rely on the Record base class to compute this flag for you, or + /// you may set it to true or false when you know better. Setting it to + /// false does not prevent it from turning true on subsequent modifications + /// of the record. + public var hasPersistentChangedValues: Bool { + get { return makePersistentChangedValuesIterator().next() != nil } + set { referenceRow = newValue ? nil : Row(self) } + } + + /// A dictionary of changes that have not been saved. + /// + /// Its keys are column names, and values the old values that have been + /// changed since last fetching or saving of the record. + /// + /// Unless the record has actually been fetched or saved, the old values + /// are nil. + /// + /// See `hasPersistentChangedValues` for more information. + public var persistentChangedValues: [String: DatabaseValue?] { + var persistentChangedValues: [String: DatabaseValue?] = [:] + + for (key, value) in makePersistentChangedValuesIterator() { + persistentChangedValues[key] = value + } + return persistentChangedValues + } + + // A change iterator that is used by both hasPersistentChangedValues and + // persistentChangedValues properties. + private func makePersistentChangedValuesIterator() -> AnyIterator<(column: String, old: DatabaseValue?)> { + let oldRow = referenceRow + var newValueIterator = PersistenceContainer(self).makeIterator() + return AnyIterator { + // Loop until we find a change, or exhaust columns: + while let (column, newValue) = newValueIterator.next() { + let new = newValue?.databaseValue ?? .null + guard let oldRow = oldRow, let old: DatabaseValue = oldRow[column] else { + return (column: column, old: nil) + } + if new != old { + return (column: column, old: old) + } + } + return nil + } + } + + + /// Reference row for the *hasPersistentChangedValues* property. + var referenceRow: Row? + + + // MARK: - CRUD + + /// Executes an INSERT statement. + /// + /// On success, this method sets the *hasPersistentChangedValues* flag + /// to false. + /// + /// This method is guaranteed to have inserted a row in the database if it + /// returns without error. + /// + /// Records whose primary key is declared as "INTEGER PRIMARY KEY" have + /// their id automatically set after successful insertion, if it was nil + /// before the insertion. + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError whenever an SQLite error occurs. + open func insert(_ db: Database) throws { + let conflictResolutionForInsert = type(of: self).persistenceConflictPolicy.conflictResolutionForInsert + let dao = try DAO(db, self) + var persistenceContainer = dao.persistenceContainer + try dao.insertStatement(onConflict: conflictResolutionForInsert).execute() + + if !conflictResolutionForInsert.invalidatesLastInsertedRowID { + let rowID = db.lastInsertedRowID + let rowIDColumn = dao.primaryKey.rowIDColumn + didInsert(with: rowID, for: rowIDColumn) + + // Update persistenceContainer with inserted id, so that we can + // set hasPersistentChangedValues to false: + if let rowIDColumn = rowIDColumn { + persistenceContainer[caseInsensitive: rowIDColumn] = rowID + } + } + + // Set hasPersistentChangedValues to false + referenceRow = Row(persistenceContainer) + } + + /// Executes an UPDATE statement. + /// + /// On success, this method sets the *hasPersistentChangedValues* flag + /// to false. + /// + /// This method is guaranteed to have updated a row in the database if it + /// returns without error. + /// + /// - parameter db: A database connection. + /// - parameter columns: The columns to update. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + /// PersistenceError.recordNotFound is thrown if the primary key does not + /// match any row in the database and record could not be updated. + open func update(_ db: Database, columns: Set<String>) throws { + // The simplest code would be: + // + // try performUpdate(db, columns: columns) + // hasPersistentChangedValues = false + // + // But this would trigger two calls to `encode(to:)`. + let dao = try DAO(db, self) + guard let statement = try dao.updateStatement(columns: columns, onConflict: type(of: self).persistenceConflictPolicy.conflictResolutionForUpdate) else { + // Nil primary key + throw PersistenceError.recordNotFound(self) + } + try statement.execute() + if db.changesCount == 0 { + throw PersistenceError.recordNotFound(self) + } + + // Set hasPersistentChangedValues to false + referenceRow = Row(dao.persistenceContainer) + } + + /// If the record has been changed, executes an UPDATE statement so that + /// those changes and only those changes are saved in the database. + /// + /// On success, this method sets the *hasPersistentChangedValues* flag + /// to false. + /// + /// This method is guaranteed to have saved the eventual changes in the + /// database if it returns without error. + /// + /// - parameter db: A database connection. + /// - parameter columns: The columns to update. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + /// PersistenceError.recordNotFound is thrown if the primary key does not + /// match any row in the database and record could not be updated. + final public func updateChanges(_ db: Database) throws { + let changedColumns = Set(persistentChangedValues.keys) + guard !changedColumns.isEmpty else { + return + } + try update(db, columns: changedColumns) + } + + /// Executes an INSERT or an UPDATE statement so that `self` is saved in + /// the database. + /// + /// If the record has a non-nil primary key and a matching row in the + /// database, this method performs an update. + /// + /// Otherwise, performs an insert. + /// + /// On success, this method sets the *hasPersistentChangedValues* flag + /// to false. + /// + /// This method is guaranteed to have inserted or updated a row in the + /// database if it returns without error. + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError whenever an SQLite error occurs, or errors + /// thrown by update(). + final public func save(_ db: Database) throws { + try performSave(db) + } + + /// Executes a DELETE statement. + /// + /// On success, this method sets the *hasPersistentChangedValues* flag + /// to true. + /// + /// - parameter db: A database connection. + /// - returns: Whether a database row was deleted. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + @discardableResult + open func delete(_ db: Database) throws -> Bool { + defer { + // Future calls to update() will throw NotFound. Make the user + // a favor and make sure this error is thrown even if she checks the + // hasPersistentChangedValues flag: + hasPersistentChangedValues = true + } + return try performDelete(db) + } +} diff --git a/Pods/GRDB.swift/GRDB/Record/RowConvertible+Decodable.swift b/Pods/GRDB.swift/GRDB/Record/RowConvertible+Decodable.swift new file mode 100644 index 0000000..b1f5065 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Record/RowConvertible+Decodable.swift @@ -0,0 +1,217 @@ +private struct RowKeyedDecodingContainer<Key: CodingKey>: KeyedDecodingContainerProtocol { + let row: Row + + init(row: Row) { + self.row = row + } + + /// The path of coding keys taken to get to this point in decoding. + /// A `nil` value indicates an unkeyed container. + var codingPath: [CodingKey] { return [] } + + /// All the keys the `Decoder` has for this container. + /// + /// Different keyed containers from the same `Decoder` may return different keys here; it is possible to encode with multiple key types which are not convertible to one another. This should report all keys present which are convertible to the requested type. + var allKeys: [Key] { + return row.columnNames.flatMap { Key(stringValue: $0) } + } + + /// Returns whether the `Decoder` contains a value associated with the given key. + /// + /// The value associated with the given key may be a null value as appropriate for the data format. + /// + /// - parameter key: The key to search for. + /// - returns: Whether the `Decoder` has an entry for the given key. + func contains(_ key: Key) -> Bool { + return row.hasColumn(key.stringValue) + } + + /// Decodes a null value for the given key. + /// + /// - parameter key: The key that the decoded value is associated with. + /// - returns: Whether the encountered value was null. + /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry for the given key. + func decodeNil(forKey key: Key) throws -> Bool { + return row[key.stringValue] == nil + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key and convertible to the requested type. + /// - throws: `DecodingError.typeMismatch` if the encountered encoded value is not convertible to the requested type. + /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry for the given key. + /// - throws: `DecodingError.valueNotFound` if `self` has a null entry for the given key. + func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { return row[key.stringValue] } + func decode(_ type: Int.Type, forKey key: Key) throws -> Int { return row[key.stringValue] } + func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { return row[key.stringValue] } + func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { return row[key.stringValue] } + func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { return row[key.stringValue] } + func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { return row[key.stringValue] } + func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { return row[key.stringValue] } + func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { return row[key.stringValue] } + func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { return row[key.stringValue] } + func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { return row[key.stringValue] } + func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { return row[key.stringValue] } + func decode(_ type: Float.Type, forKey key: Key) throws -> Float { return row[key.stringValue] } + func decode(_ type: Double.Type, forKey key: Key) throws -> Double { return row[key.stringValue] } + func decode(_ type: String.Type, forKey key: Key) throws -> String { return row[key.stringValue] } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key and convertible to the requested type. + /// - throws: `DecodingError.typeMismatch` if the encountered encoded value is not convertible to the requested type. + /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry for the given key. + /// - throws: `DecodingError.valueNotFound` if `self` has a null entry for the given key. + func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable { + let dbValue: DatabaseValue = row[key.stringValue] + if let type = T.self as? DatabaseValueConvertible.Type { + // Prefer DatabaseValueConvertible decoding over Decodable. + // This allows decoding Date from String, or DatabaseValue from NULL. + return type.fromDatabaseValue(dbValue) as! T + } else { + return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) + } + } + + /// Returns the data stored for the given key as represented in a container keyed by the given key type. + /// + /// - parameter type: The key type to use for the container. + /// - parameter key: The key that the nested container is associated with. + /// - returns: A keyed decoding container view into `self`. + /// - throws: `DecodingError.typeMismatch` if the encountered stored value is not a keyed container. + func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey { + fatalError("Not implemented") + } + + /// Returns the data stored for the given key as represented in an unkeyed container. + /// + /// - parameter key: The key that the nested container is associated with. + /// - returns: An unkeyed decoding container view into `self`. + /// - throws: `DecodingError.typeMismatch` if the encountered stored value is not an unkeyed container. + func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + fatalError("Not implemented") + } + + /// Returns a `Decoder` instance for decoding `super` from the container associated with the default `super` key. + /// + /// Equivalent to calling `superDecoder(forKey:)` with `Key(stringValue: "super", intValue: 0)`. + /// + /// - returns: A new `Decoder` to pass to `super.init(from:)`. + /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry for the default `super` key. + /// - throws: `DecodingError.valueNotFound` if `self` has a null entry for the default `super` key. + public func superDecoder() throws -> Decoder { + fatalError("Not implemented") + } + + /// Returns a `Decoder` instance for decoding `super` from the container associated with the given key. + /// + /// - parameter key: The key to decode `super` for. + /// - returns: A new `Decoder` to pass to `super.init(from:)`. + /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry for the given key. + /// - throws: `DecodingError.valueNotFound` if `self` has a null entry for the given key. + public func superDecoder(forKey key: Key) throws -> Decoder { + fatalError("Not implemented") + } +} + +private struct RowSingleValueDecodingContainer: SingleValueDecodingContainer { + let row: Row + let column: CodingKey + + var codingPath: [CodingKey] { return [] } + + /// Decodes a null value. + /// + /// - returns: Whether the encountered value was null. + func decodeNil() -> Bool { + return row[column.stringValue] == nil + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the encountered encoded value cannot be converted to the requested type. + /// - throws: `DecodingError.valueNotFound` if the encountered encoded value is null. + func decode(_ type: Bool.Type) throws -> Bool { return row[column.stringValue] } + func decode(_ type: Int.Type) throws -> Int { return row[column.stringValue] } + func decode(_ type: Int8.Type) throws -> Int8 { return row[column.stringValue] } + func decode(_ type: Int16.Type) throws -> Int16 { return row[column.stringValue] } + func decode(_ type: Int32.Type) throws -> Int32 { return row[column.stringValue] } + func decode(_ type: Int64.Type) throws -> Int64 { return row[column.stringValue] } + func decode(_ type: UInt.Type) throws -> UInt { return row[column.stringValue] } + func decode(_ type: UInt8.Type) throws -> UInt8 { return row[column.stringValue] } + func decode(_ type: UInt16.Type) throws -> UInt16 { return row[column.stringValue] } + func decode(_ type: UInt32.Type) throws -> UInt32 { return row[column.stringValue] } + func decode(_ type: UInt64.Type) throws -> UInt64 { return row[column.stringValue] } + func decode(_ type: Float.Type) throws -> Float { return row[column.stringValue] } + func decode(_ type: Double.Type) throws -> Double { return row[column.stringValue] } + func decode(_ type: String.Type) throws -> String { return row[column.stringValue] } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the encountered encoded value cannot be converted to the requested type. + /// - throws: `DecodingError.valueNotFound` if the encountered encoded value is null. + func decode<T>(_ type: T.Type) throws -> T where T : Decodable { + if let type = T.self as? DatabaseValueConvertible.Type { + // Prefer DatabaseValueConvertible decoding over Decodable. + // This allows decoding Date from String, or DatabaseValue from NULL. + return type.fromDatabaseValue(row[column.stringValue]) as! T + } else { + return try T(from: RowDecoder(row: row, codingPath: [column])) + } + } +} + +private struct RowDecoder: Decoder { + let row: Row + + init(row: Row, codingPath: [CodingKey]) { + self.row = row + self.codingPath = codingPath + } + + // Decoder + let codingPath: [CodingKey] + var userInfo: [CodingUserInfoKey : Any] { return [:] } + + func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> { + // Asked for a keyed type: top level required + guard codingPath.isEmpty else { + throw DecodingError.typeMismatch( + RowDecoder.self, + DecodingError.Context(codingPath: codingPath, debugDescription: "nested decoding is not supported")) + } + + return KeyedDecodingContainer(RowKeyedDecodingContainer<Key>(row: row)) + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + throw DecodingError.typeMismatch( + UnkeyedDecodingContainer.self, + DecodingError.Context(codingPath: codingPath, debugDescription: "unkeyed decoding is not supported")) + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + // Asked for a value type: column name required + guard let codingKey = codingPath.last else { + throw DecodingError.typeMismatch( + RowDecoder.self, + DecodingError.Context(codingPath: codingPath, debugDescription: "single value decoding requires a coding key")) + } + return RowSingleValueDecodingContainer(row: row, column: codingKey) + } +} + +extension RowConvertible where Self: Decodable { + /// Initializes a record from `row`. + public init(row: Row) { + try! self.init(from: RowDecoder(row: row, codingPath: [])) + } +} diff --git a/Pods/GRDB.swift/GRDB/Record/RowConvertible+TableMapping.swift b/Pods/GRDB.swift/GRDB/Record/RowConvertible+TableMapping.swift new file mode 100644 index 0000000..e0c03f4 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Record/RowConvertible+TableMapping.swift @@ -0,0 +1,179 @@ +extension RowConvertible where Self: TableMapping { + + // MARK: Fetching All + + /// A cursor over all records fetched from the database. + /// + /// // SELECT * FROM players + /// let players = try Player.fetchCursor(db) // Cursor of Player + /// while let player = try players.next() { // Player + /// ... + /// } + /// + /// Records are iterated in the natural ordering of the table. + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// The selection defaults to all columns. This default can be changed for + /// all requests by the `TableMapping.databaseSelection` property, or + /// for individual requests with the `TableMapping.select` method. + /// + /// - parameter db: A database connection. + /// - returns: A cursor over fetched records. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ db: Database) throws -> RecordCursor<Self> { + return try all().fetchCursor(db) + } + + /// An array of all records fetched from the database. + /// + /// // SELECT * FROM players + /// let players = try Player.fetchAll(db) // [Player] + /// + /// The selection defaults to all columns. This default can be changed for + /// all requests by the `TableMapping.databaseSelection` property, or + /// for individual requests with the `TableMapping.select` method. + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ db: Database) throws -> [Self] { + return try all().fetchAll(db) + } + + /// The first found record. + /// + /// // SELECT * FROM players + /// let player = try Player.fetchOne(db) // Player? + /// + /// The selection defaults to all columns. This default can be changed for + /// all requests by the `TableMapping.databaseSelection` property, or + /// for individual requests with the `TableMapping.select` method. + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchOne(_ db: Database) throws -> Self? { + return try all().fetchOne(db) + } +} + +extension RowConvertible where Self: TableMapping { + + // MARK: Fetching by Single-Column Primary Key + + /// Returns a cursor over records, given their primary keys. + /// + /// let players = try Player.fetchCursor(db, keys: [1, 2, 3]) // Cursor of Player + /// while let player = try players.next() { // Player + /// ... + /// } + /// + /// Records are iterated in unspecified order. + /// + /// - parameters: + /// - db: A database connection. + /// - keys: A sequence of primary keys. + /// - returns: A cursor over fetched records. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor<Sequence: Swift.Sequence>(_ db: Database, keys: Sequence) throws -> RecordCursor<Self> where Sequence.Element: DatabaseValueConvertible { + return try filter(db, keys: keys).fetchCursor(db) + } + + /// Returns an array of records, given their primary keys. + /// + /// let players = try Player.fetchAll(db, keys: [1, 2, 3]) // [Player] + /// + /// The order of records in the returned array is undefined. + /// + /// - parameters: + /// - db: A database connection. + /// - keys: A sequence of primary keys. + /// - returns: An array of records. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll<Sequence: Swift.Sequence>(_ db: Database, keys: Sequence) throws -> [Self] where Sequence.Element: DatabaseValueConvertible { + let keys = Array(keys) + if keys.isEmpty { + // Avoid hitting the database + return [] + } + return try filter(db, keys: keys).fetchAll(db) + } + + /// Returns a single record given its primary key. + /// + /// let player = try Player.fetchOne(db, key: 123) // Player? + /// + /// - parameters: + /// - db: A database connection. + /// - key: A primary key value. + /// - returns: An optional record. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchOne<PrimaryKeyType: DatabaseValueConvertible>(_ db: Database, key: PrimaryKeyType?) throws -> Self? { + guard let key = key else { + // Avoid hitting the database + return nil + } + return try filter(db, keys: [key]).fetchOne(db) + } +} + +extension RowConvertible where Self: TableMapping { + + // MARK: Fetching by Key + + /// Returns a cursor over records identified by the provided unique keys + /// (primary key or any key with a unique index on it). + /// + /// let players = try Player.fetchCursor(db, keys: [["email": "a@example.com"], ["email": "b@example.com"]]) // Cursor of Player + /// while let player = try players.next() { // Player + /// ... + /// } + /// + /// Records are iterated in unspecified order. + /// + /// - parameters: + /// - db: A database connection. + /// - keys: An array of key dictionaries. + /// - returns: A cursor over fetched records. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ db: Database, keys: [[String: DatabaseValueConvertible?]]) throws -> RecordCursor<Self> { + return try filter(db, keys: keys).fetchCursor(db) + } + + /// Returns an array of records identified by the provided unique keys + /// (primary key or any key with a unique index on it). + /// + /// let players = try Player.fetchAll(db, keys: [["email": "a@example.com"], ["email": "b@example.com"]]) // [Player] + /// + /// The order of records in the returned array is undefined. + /// + /// - parameters: + /// - db: A database connection. + /// - keys: An array of key dictionaries. + /// - returns: An array of records. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ db: Database, keys: [[String: DatabaseValueConvertible?]]) throws -> [Self] { + let keys = Array(keys) + if keys.isEmpty { + // Avoid hitting the database + return [] + } + return try filter(db, keys: keys).fetchAll(db) + } + + /// Returns a single record identified by a unique key (the primary key or + /// any key with a unique index on it). + /// + /// let player = try Player.fetchOne(db, key: ["name": Arthur"]) // Player? + /// + /// - parameters: + /// - db: A database connection. + /// - key: A dictionary of values. + /// - returns: An optional record. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchOne(_ db: Database, key: [String: DatabaseValueConvertible?]) throws -> Self? { + return try filter(db, keys: [key]).fetchOne(db) + } +} diff --git a/Pods/GRDB.swift/GRDB/Record/RowConvertible.swift b/Pods/GRDB.swift/GRDB/Record/RowConvertible.swift new file mode 100644 index 0000000..ce2fbd0 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Record/RowConvertible.swift @@ -0,0 +1,238 @@ +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif + +/// Types that adopt RowConvertible can be initialized from a database Row. +/// +/// let row = try Row.fetchOne(db, "SELECT ...")! +/// let player = Player(row) +/// +/// The protocol comes with built-in methods that allow to fetch cursors, +/// arrays, or single records: +/// +/// try Player.fetchCursor(db, "SELECT ...", arguments:...) // Cursor of Player +/// try Player.fetchAll(db, "SELECT ...", arguments:...) // [Player] +/// try Player.fetchOne(db, "SELECT ...", arguments:...) // Player? +/// +/// let statement = try db.makeSelectStatement("SELECT ...") +/// try Player.fetchCursor(statement, arguments:...) // Cursor of Player +/// try Player.fetchAll(statement, arguments:...) // [Player] +/// try Player.fetchOne(statement, arguments:...) // Player? +/// +/// RowConvertible is adopted by Record. +public protocol RowConvertible { + + /// Initializes a record from `row`. + /// + /// For performance reasons, the row argument may be reused during the + /// iteration of a fetch query. If you want to keep the row for later use, + /// make sure to store a copy: `self.row = row.copy()`. + init(row: Row) +} + +/// A cursor of records. For example: +/// +/// struct Player : RowConvertible { ... } +/// try dbQueue.inDatabase { db in +/// let players: RecordCursor<Player> = try Player.fetchCursor(db, "SELECT * FROM players") +/// } +public final class RecordCursor<Record: RowConvertible> : Cursor { + private let statement: SelectStatement + private let row: Row // Reused for performance + private let sqliteStatement: SQLiteStatement + private var done = false + + init(statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws { + self.statement = statement + self.row = try Row(statement: statement).adapted(with: adapter, layout: statement) + self.sqliteStatement = statement.sqliteStatement + statement.cursorReset(arguments: arguments) + } + + public func next() throws -> Record? { + if done { return nil } + switch sqlite3_step(sqliteStatement) { + case SQLITE_DONE: + done = true + return nil + case SQLITE_ROW: + return Record(row: row) + case let code: + statement.database.selectStatementDidFail(statement) + throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments) + } + } +} + +extension RowConvertible { + + // MARK: Fetching From SelectStatement + + /// A cursor over records fetched from a prepared statement. + /// + /// let statement = try db.makeSelectStatement("SELECT * FROM players") + /// let players = try Player.fetchCursor(statement) // Cursor of Player + /// while let player = try players.next() { // Player + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - statement: The statement to run. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: A cursor over fetched records. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> RecordCursor<Self> { + return try RecordCursor(statement: statement, arguments: arguments, adapter: adapter) + } + + /// Returns an array of records fetched from a prepared statement. + /// + /// let statement = try db.makeSelectStatement("SELECT * FROM players") + /// let players = try Player.fetchAll(statement) // [Player] + /// + /// - parameters: + /// - statement: The statement to run. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An array of records. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Self] { + return try Array(fetchCursor(statement, arguments: arguments, adapter: adapter)) + } + + /// Returns a single record fetched from a prepared statement. + /// + /// let statement = try db.makeSelectStatement("SELECT * FROM players") + /// let player = try Player.fetchOne(statement) // Player? + /// + /// - parameters: + /// - statement: The statement to run. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An optional record. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchOne(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Self? { + return try fetchCursor(statement, arguments: arguments, adapter: adapter).next() + } +} + +extension RowConvertible { + + // MARK: Fetching From Request + + /// Returns a cursor over records fetched from a fetch request. + /// + /// let nameColumn = Column("firstName") + /// let request = Player.order(nameColumn) + /// let identities = try Identity.fetchCursor(db, request) // Cursor of Identity + /// while let identity = try identities.next() { // Identity + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - db: A database connection. + /// - request: A fetch request. + /// - returns: A cursor over fetched records. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ db: Database, _ request: Request) throws -> RecordCursor<Self> { + let (statement, adapter) = try request.prepare(db) + return try fetchCursor(statement, adapter: adapter) + } + + /// Returns an array of records fetched from a fetch request. + /// + /// let nameColumn = Column("name") + /// let request = Player.order(nameColumn) + /// let identities = try Identity.fetchAll(db, request) // [Identity] + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ db: Database, _ request: Request) throws -> [Self] { + let (statement, adapter) = try request.prepare(db) + return try fetchAll(statement, adapter: adapter) + } + + /// Returns a single record fetched from a fetch request. + /// + /// let nameColumn = Column("name") + /// let request = Player.order(nameColumn) + /// let identity = try Identity.fetchOne(db, request) // Identity? + /// + /// - parameter db: A database connection. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchOne(_ db: Database, _ request: Request) throws -> Self? { + let (statement, adapter) = try request.prepare(db) + return try fetchOne(statement, adapter: adapter) + } +} + +extension RowConvertible { + + // MARK: Fetching From SQL + + /// Returns a cursor over records fetched from an SQL query. + /// + /// let players = try Player.fetchCursor(db, "SELECT * FROM players") // Cursor of Player + /// while let player = try players.next() { // Player + /// ... + /// } + /// + /// If the database is modified during the cursor iteration, the remaining + /// elements are undefined. + /// + /// The cursor must be iterated in a protected dispath queue. + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: A cursor over fetched records. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> RecordCursor<Self> { + return try fetchCursor(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) + } + + /// Returns an array of records fetched from an SQL query. + /// + /// let players = try Player.fetchAll(db, "SELECT * FROM players") // [Player] + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An array of records. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchAll(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> [Self] { + return try fetchAll(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) + } + + /// Returns a single record fetched from an SQL query. + /// + /// let player = try Player.fetchOne(db, "SELECT * FROM players") // Player? + /// + /// - parameters: + /// - db: A database connection. + /// - sql: An SQL query. + /// - arguments: Optional statement arguments. + /// - adapter: Optional RowAdapter + /// - returns: An optional record. + /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. + public static func fetchOne(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Self? { + return try fetchOne(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) + } +} diff --git a/Pods/GRDB.swift/GRDB/Record/TableMapping.swift b/Pods/GRDB.swift/GRDB/Record/TableMapping.swift new file mode 100644 index 0000000..6a4abfa --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Record/TableMapping.swift @@ -0,0 +1,132 @@ +/// Types that adopt TableMapping declare a particular relationship with +/// a database table. +/// +/// Types that adopt both TableMapping and RowConvertible are granted with +/// built-in methods that allow to fetch instances identified by key: +/// +/// try Player.fetchOne(db, key: 123) // Player? +/// try Citizenship.fetchOne(db, key: ["citizenId": 12, "countryId": 45]) // Citizenship? +/// +/// TableMapping is adopted by Record. +public protocol TableMapping { + /// The name of the database table used to build requests. + /// + /// struct Player : TableMapping { + /// static var databaseTableName = "players" + /// } + /// + /// // SELECT * FROM players + /// try Player.fetchAll(db) + static var databaseTableName: String { get } + + /// The default request selection. + /// + /// Unless said otherwise, requests select all columns: + /// + /// // SELECT * FROM players + /// try Player.fetchAll(db) + /// + /// You can provide a custom implementation and provide an explicit list + /// of columns: + /// + /// struct RestrictedPlayer : TableMapping { + /// static var databaseTableName = "players" + /// static var databaseSelection = [Column("id"), Column("name")] + /// } + /// + /// // SELECT id, name FROM players + /// try RestrictedPlayer.fetchAll(db) + /// + /// You can also add extra columns such as the `rowid` column: + /// + /// struct ExtendedPlayer : TableMapping { + /// static var databaseTableName = "players" + /// static let databaseSelection: [SQLSelectable] = [AllColumns(), Column.rowID] + /// } + /// + /// // SELECT *, rowid FROM players + /// try ExtendedPlayer.fetchAll(db) + static var databaseSelection: [SQLSelectable] { get } +} + +extension TableMapping { + /// Default value: `[AllColumns()]`. + public static var databaseSelection: [SQLSelectable] { + return [AllColumns()] + } +} + +extension TableMapping { + + // MARK: Counting All + + /// The number of records. + /// + /// - parameter db: A database connection. + public static func fetchCount(_ db: Database) throws -> Int { + return try all().fetchCount(db) + } +} + +extension TableMapping { + + // MARK: Key Requests + + static func filter<Sequence: Swift.Sequence>(_ db: Database, keys: Sequence) throws -> QueryInterfaceRequest<Self> where Sequence.Element: DatabaseValueConvertible { + let primaryKey = try db.primaryKey(databaseTableName) + let columns = primaryKey.columns.map { Column($0) } + GRDBPrecondition(columns.count == 1, "table \(databaseTableName) has multiple columns in its primary key") + let column = columns[0] + + let keys = Array(keys) + switch keys.count { + case 0: + return none() + case 1: + return filter(column == keys[0]) + default: + return filter(keys.contains(column)) + } + } + + // Raises a fatal error if there is no unique index on the columns (unless + // fatalErrorOnMissingUniqueIndex is false, for testability). + // + // TODO: think about + // - allowing non unique keys in Type.fetchOne(db, key: ...) ??? + // - allowing non unique keys in Type.fetchAll/Cursor(db, keys: ...) + // - forbidding nil values: Player.deleteOne(db, key: ["email": nil]) may delete several rows (case of a nullable unique key) + static func filter(_ db: Database, keys: [[String: DatabaseValueConvertible?]], fatalErrorOnMissingUniqueIndex: Bool = true) throws -> QueryInterfaceRequest<Self> { + // SELECT * FROM table WHERE ((a=? AND b=?) OR (c=? AND d=?) OR ...) + let keyPredicates: [SQLExpression] = try keys.map { key in + // Prevent filter(db, keys: [[:]]) + GRDBPrecondition(!key.isEmpty, "Invalid empty key dictionary") + + // Prevent filter(db, keys: [["foo": 1, "bar": 2]]) where + // ("foo", "bar") is not a unique key (primary key or columns of a + // unique index) + guard let orderedColumns = try db.columnsForUniqueKey(key.keys, in: databaseTableName) else { + let message = "table \(databaseTableName) has no unique index on column(s) \(key.keys.sorted().joined(separator: ", "))" + if fatalErrorOnMissingUniqueIndex { + fatalError(message) + } else { + throw DatabaseError(resultCode: .SQLITE_MISUSE, message: message) + } + } + + let lowercaseOrderedColumns = orderedColumns.map { $0.lowercased() } + let columnPredicates: [SQLExpression] = key + // Sort key columns in the same order as the unique index + .sorted { (kv1, kv2) in lowercaseOrderedColumns.index(of: kv1.0.lowercased())! < lowercaseOrderedColumns.index(of: kv2.0.lowercased())! } + .map { (column, value) in Column(column) == value } + return SQLBinaryOperator.and.join(columnPredicates)! // not nil because columnPredicates is not empty + } + + guard let predicate = SQLBinaryOperator.or.join(keyPredicates) else { + // No key + return none() + } + + return filter(predicate) + } +} diff --git a/Pods/GRDB.swift/GRDB/Utils/Pool.swift b/Pods/GRDB.swift/GRDB/Utils/Pool.swift new file mode 100644 index 0000000..bcc68f8 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Utils/Pool.swift @@ -0,0 +1,121 @@ +import Dispatch + +/// A Pool maintains a set of elements that are built them on demand. A pool has +/// a maximum number of elements. +/// +/// // A pool of 3 integers +/// var number = 0 +/// let pool = Pool<Int>(maximumCount: 3, makeElement: { +/// number = number + 1 +/// return number +/// }) +/// +/// The function get() dequeues an available element and gives this element to +/// the block argument. During the block execution, the element is not +/// available. When the block is ended, the element is available again. +/// +/// // got 1 +/// pool.get { n in +/// print("got \(n)") +/// } +/// +/// If there is no available element, the pool builds a new element, unless the +/// maximum number of elements is reached. In this case, the get() method +/// blocks the current thread, until an element eventually turns available again. +/// +/// DispatchQueue.concurrentPerform(iterations: 6) { _ in +/// pool.get { n in +/// print("got \(n)") +/// } +/// } +/// +/// got 1 +/// got 2 +/// got 3 +/// got 2 +/// got 1 +/// got 3 +final class Pool<T> { + private class Item { + let element: T + var available: Bool + + init(element: T, available: Bool) { + self.element = element + self.available = available + } + } + + private let makeElement: () throws -> T + private var items: ReadWriteBox<[Item]> = ReadWriteBox([]) + private let semaphore: DispatchSemaphore // limits the number of elements + + init(maximumCount: Int, makeElement: @escaping () throws -> T) { + GRDBPrecondition(maximumCount > 0, "Pool size must be at least 1") + self.makeElement = makeElement + self.semaphore = DispatchSemaphore(value: maximumCount) + } + + /// Returns a tuple (element, release) + /// Client MUST call release() after the element has been used. + func get() throws -> (T, () -> ()) { + _ = semaphore.wait(timeout: .distantFuture) + var item: Item! = nil + do { + try items.write { items in + if let availableItem = items.first(where: { $0.available }) { + item = availableItem + item.available = false + } else { + let element = try makeElement() + item = Item(element: element, available: false) + items.append(item) + } + } + } catch { + semaphore.signal() + throw error + } + let release = { + self.items.write { _ in + // This is why Item is a class, not a struct: so that we can + // release it without having to find in it the items array. + item.available = true + } + self.semaphore.signal() + } + return (item.element, release) + } + + /// Performs a synchronous block with an element. The element turns + /// available after the block has executed. + func get<U>(block: (T) throws -> U) throws -> U { + let (element, release) = try get() + defer { release() } + return try block(element) + } + + /// Performs a block on each pool element, available or not. + /// The block is run is some arbitrary dispatch queue. + func forEach(_ body: (T) throws -> ()) rethrows { + try items.read { items in + for item in items { + try body(item.element) + } + } + } + + /// Empty the pool. Currently used items won't be reused. + func clear() { + clear {} + } + + /// Empty the pool. Currently used items won't be reused. + /// Eventual block is executed before any other element is dequeued. + func clear(andThen block: () throws -> ()) rethrows { + try items.write { items in + items.removeAll() + try block() + } + } +} diff --git a/Pods/GRDB.swift/GRDB/Utils/ReadWriteBox.swift b/Pods/GRDB.swift/GRDB/Utils/ReadWriteBox.swift new file mode 100644 index 0000000..00ff4a0 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Utils/ReadWriteBox.swift @@ -0,0 +1,29 @@ +import Dispatch + +/// A ReadWriteBox grants multiple readers and single-writer guarantees on a value. +final class ReadWriteBox<T> { + var value: T { + get { return read { $0 } } + set { write { $0 = newValue } } + } + + init(_ value: T) { + self._value = value + self.queue = DispatchQueue(label: "GRDB.ReadWriteBox", attributes: [.concurrent]) + } + + func read<U>(_ block: (T) throws -> U) rethrows -> U { + return try queue.sync { + try block(_value) + } + } + + func write(_ block: (inout T) throws -> Void) rethrows { + try queue.sync(flags: [.barrier]) { + try block(&_value) + } + } + + private var _value: T + private var queue: DispatchQueue +} diff --git a/Pods/GRDB.swift/GRDB/Utils/Result.swift b/Pods/GRDB.swift/GRDB/Utils/Result.swift new file mode 100644 index 0000000..b388fe1 --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Utils/Result.swift @@ -0,0 +1,40 @@ +enum Result<Value> { + case success(Value) + case failure(Error) + + init(value: () throws -> Value) { + do { + self = try .success(value()) + } catch { + self = .failure(error) + } + } + + /// Evaluates the given closure when this `Result` is a success, passing the + /// unwrapped value as a parameter. + /// + /// Use the `map` method with a closure that does not throw. For example: + /// + /// let possibleData: Result<Data> = .success(Data()) + /// let possibleInt = possibleData.map { $0.count } + /// try print(possibleInt.unwrap()) + /// // Prints "0" + /// + /// let noData: Result<Data> = .failure(error) + /// let noInt = noData.map { $0.count } + /// try print(noInt.unwrap()) + /// // Throws error + /// + /// - parameter transform: A closure that takes the success value of + /// the instance. + /// - returns: A `Result` containing the result of the given closure. If + /// this instance is a failure, returns the same failure. + func map<T>(_ transform: (Value) -> T) -> Result<T> { + switch self { + case .success(let value): + return .success(transform(value)) + case .failure(let error): + return .failure(error) + } + } +} diff --git a/Pods/GRDB.swift/GRDB/Utils/Utils.swift b/Pods/GRDB.swift/GRDB/Utils/Utils.swift new file mode 100644 index 0000000..92281cd --- /dev/null +++ b/Pods/GRDB.swift/GRDB/Utils/Utils.swift @@ -0,0 +1,65 @@ +import Foundation + +// MARK: - Public + +extension String { + /// Returns the receiver, quoted for safe insertion as an identifier in an + /// SQL query. + /// + /// db.execute("SELECT * FROM \(tableName.quotedDatabaseIdentifier)") + public var quotedDatabaseIdentifier: String { + // See https://www.sqlite.org/lang_keywords.html + return "\"" + self + "\"" + } +} + +/// Return as many question marks separated with commas as the *count* argument. +/// +/// databaseQuestionMarks(count: 3) // "?,?,?" +public func databaseQuestionMarks(count: Int) -> String { + return Array(repeating: "?", count: count).joined(separator: ",") +} + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// This protocol is an implementation detail of GRDB. Don't use it. +public protocol _OptionalProtocol { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + associatedtype _Wrapped +} + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// This conformance is an implementation detail of GRDB. Don't rely on it. +extension Optional : _OptionalProtocol { + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public typealias _Wrapped = Wrapped +} + + +// MARK: - Internal + +/// Reserved for GRDB: do not use. +func GRDBPrecondition(_ condition: @autoclosure() -> Bool, _ message: @autoclosure() -> String = "", file: StaticString = #file, line: UInt = #line) { + /// Custom precondition function which aims at solving + /// https://bugs.swift.org/browse/SR-905 and + /// https://github.com/groue/GRDB.swift/issues/37 + if !condition() { + fatalError(message, file: file, line: line) + } +} + +// Workaround Swift inconvenience around factory methods of non-final classes +func cast<T, U>(_ value: T) -> U? { + return value as? U +} + +extension Array { + /// Removes the first object that matches *predicate*. + mutating func removeFirst(_ predicate: (Element) throws -> Bool) rethrows { + if let index = try index(where: predicate) { + remove(at: index) + } + } +} + diff --git a/Pods/GRDB.swift/LICENSE b/Pods/GRDB.swift/LICENSE new file mode 100644 index 0000000..b8871b0 --- /dev/null +++ b/Pods/GRDB.swift/LICENSE @@ -0,0 +1,7 @@ +Copyright (C) 2017 Gwendal Roué + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Pods/GRDB.swift/README.md b/Pods/GRDB.swift/README.md new file mode 100644 index 0000000..455fed1 --- /dev/null +++ b/Pods/GRDB.swift/README.md @@ -0,0 +1,6150 @@ +GRDB 2.0 [![Swift](https://img.shields.io/badge/swift-4-orange.svg?style=flat)](https://developer.apple.com/swift/) [![Platforms](https://img.shields.io/cocoapods/p/GRDB.swift.svg)](https://developer.apple.com/swift/) [![License](https://img.shields.io/github/license/groue/GRDB.swift.svg?maxAge=2592000)](/LICENSE) [![Build Status](https://travis-ci.org/groue/GRDB.swift.svg?branch=master)](https://travis-ci.org/groue/GRDB.swift) +========== + +### A toolkit for SQLite databases, with a focus on application development + +**Latest release**: September 16, 2017 • version 2.0 • [CHANGELOG](CHANGELOG.md) + +**Requirements**: iOS 8.0+ / OSX 10.9+ / watchOS 2.0+ • Swift 4.0 / Xcode 9+ + +| Swift version | GRDB version | +| ------------- | ----------------------------------------------------------- | +| Swift 2.2 | [v0.80.2](https://github.com/groue/GRDB.swift/tree/v0.80.2) | +| Swift 2.3 | [v0.81.2](https://github.com/groue/GRDB.swift/tree/v0.81.2) | +| Swift 3 | [v1.0](https://github.com/groue/GRDB.swift/tree/v1.0) | +| Swift 3.1 | [v1.3.0](https://github.com/groue/GRDB.swift/tree/v1.3.0) | +| Swift 3.2 | [v1.3.0](https://github.com/groue/GRDB.swift/tree/v1.3.0) | +| Swift 4 | [v2.0](https://github.com/groue/GRDB.swift/tree/v2.0) | + +Follow [@groue](http://twitter.com/groue) on Twitter for release announcements and usage tips. + + +## What is this? + +GRDB provides raw access to SQL and advanced SQLite features, because one sometimes enjoys a sharp tool. It has robust concurrency primitives, so that multi-threaded applications can efficiently use their databases. It grants your application models with persistence and fetching methods, so that you don't have to deal with SQL and raw database rows when you don't want to. + +Compared to [SQLite.swift](http://github.com/stephencelis/SQLite.swift) or [FMDB](http://github.com/ccgus/fmdb), GRDB can spare you a lot of glue code. Compared to [Core Data](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreData/) or [Realm](http://realm.io), it can simplify your multi-threaded applications. + +It comes with [up-to-date documentation](#documentation), [general articles](https://medium.com/@gwendal.roue), [sample code](#sample-code), and a lot of interesting resolved issues that may answer your eventual [questions](https://github.com/groue/GRDB.swift/issues?utf8=✓&q=is%3Aissue%20label%3Aquestion) and foster [best practices](https://github.com/groue/GRDB.swift/issues?q=is%3Aissue+label%3A%22best+practices%22). + + + +--- + +<p align="center"> + <a href="#features">Features</a> • + <a href="#usage">Usage</a> • + <a href="#installation">Installation</a> • + <a href="#documentation">Documentation</a> • + <a href="#faq">FAQ</a> +</p> + +--- + + +## Features + +GRDB ships with: + +- [Access to raw SQL and SQLite](#sqlite-api) +- [Records](#records): fetching and persistence methods for your custom structs and class hierarchies +- [Query Interface](#the-query-interface): a swift way to avoid the SQL language +- [WAL Mode Support](#database-pools): extra performance for multi-threaded applications +- [Migrations](#migrations): transform your database as your application evolves +- [Database Observation](#database-changes-observation): track database transactions, get notified of database changes +- [Full-Text Search](#full-text-search) +- [Encryption](#encryption) +- [Support for Custom SQLite Builds](Documentation/CustomSQLiteBuilds.md) + +Companion libraries that enhance and extend GRDB: + +- [RxGRDB](http://github.com/RxSwiftCommunity/RxGRDB): track database changes in a reactive way, with [RxSwift](https://github.com/ReactiveX/RxSwift). +- [GRDBObjc](https://github.com/groue/GRDBObjc): FMDB-compatible bindings to GRDB. + +More than a set of tools that leverage SQLite abilities, GRDB is also: + +- **Safer**: read the blog post [Four different ways to handle SQLite concurrency](https://medium.com/@gwendal.roue/four-different-ways-to-handle-sqlite-concurrency-db3bcc74d00e) +- **Faster**: see [Comparing the Performances of Swift SQLite libraries](https://github.com/groue/GRDB.swift/wiki/Performance) for a comparison between raw SQLite, FMDB, SQLite.swift, Core Data, Realm, and GRDB. + +For a general overview of how a protocol-oriented library impacts database accesses, have a look at [How to build an iOS application with SQLite and GRDB.swift](https://medium.com/@gwendal.roue/how-to-build-an-ios-application-with-sqlite-and-grdb-swift-d023a06c29b3). + + +## Usage + +Open a [connection](#database-connections) to the database: + +```swift +import GRDB + +// Simple database connection +let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite") + +// Enhanced multithreading based on SQLite's WAL mode +let dbPool = try DatabasePool(path: "/path/to/database.sqlite") +``` + +[Execute SQL statements](#executing-updates): + +```swift +try dbQueue.inDatabase { db in + try db.execute(""" + CREATE TABLE places ( + id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + favorite BOOLEAN NOT NULL DEFAULT 0, + latitude DOUBLE NOT NULL, + longitude DOUBLE NOT NULL) + """) + + try db.execute(""" + INSERT INTO places (title, favorite, latitude, longitude) + VALUES (?, ?, ?, ?) + """, arguments: ["Paris", true, 48.85341, 2.3488]) + + let parisId = db.lastInsertedRowID +} +``` + +[Fetch database rows and values](#fetch-queries): + +```swift +try dbQueue.inDatabase { db in + let rows = try Row.fetchCursor(db, "SELECT * FROM places") + while let row = try rows.next() { + let title: String = row["title"] + let isFavorite: Bool = row["favorite"] + let coordinate = CLLocationCoordinate2D( + latitude: row["latitude"], + longitude: row["longitude"]) + } + + let placeCount = try Int.fetchOne(db, "SELECT COUNT(*) FROM places")! // Int + let placeTitles = try String.fetchAll(db, "SELECT title FROM places") // [String] +} + +// Extraction +let placeCount = try dbQueue.inDatabase { db in + try Int.fetchOne(db, "SELECT COUNT(*) FROM places")! +} +``` + +Insert and fetch [records](#records): + +```swift +struct Place { + var id: Int64? + var title: String + var isFavorite: Bool + var coordinate: CLLocationCoordinate2D +} + +// snip: turn Place into a "record" by adopting the protocols that +// provide fetching and persistence methods. + +try dbQueue.inDatabase { db in + var berlin = Place( + id: nil, + title: "Berlin", + isFavorite: false, + coordinate: CLLocationCoordinate2D(latitude: 52.52437, longitude: 13.41053)) + + try berlin.insert(db) + berlin.id // some value + + berlin.isFavorite = true + try berlin.update(db) + + // Fetch [Place] from SQL + let places = try Place.fetchAll(db, "SELECT * FROM places") +} +``` + +Avoid SQL with the [query interface](#the-query-interface): + +```swift +try dbQueue.inDatabase { db in + try db.create(table: "places") { t in + t.column("id", .integer).primaryKey() + t.column("title", .text).notNull() + t.column("favorite", .boolean).notNull().defaults(to: false) + t.column("longitude", .double).notNull() + t.column("latitude", .double).notNull() + } + + // Place? + let paris = try Place.fetchOne(db, key: 1) + + // Place? + let titleColumn = Column("title") + let berlin = try Place.filter(titleColumn == "Berlin").fetchOne(db) + + // [Place] + let favoriteColumn = Column("favorite") + let favoritePlaces = try Place + .filter(favoriteColumn) + .order(titleColumn) + .fetchAll(db) +} +``` + + +Documentation +============= + +**GRDB runs on top of SQLite**: you should get familiar with the [SQLite FAQ](http://www.sqlite.org/faq.html). For general and detailed information, jump to the [SQLite Documentation](http://www.sqlite.org/docs.html). + +**Reference** + +- [GRDB Reference](http://groue.github.io/GRDB.swift/docs/2.0/index.html) (generated by [Jazzy](https://github.com/realm/jazzy)) + +**Getting Started** + +- [Installation](#installation) +- [Database Connections](#database-connections): Connect to SQLite databases + +**SQLite and SQL** + +- [SQLite API](#sqlite-api): The low-level SQLite API • [executing updates](#executing-updates) • [fetch queries](#fetch-queries) + +**Records and the Query Interface** + +- [Records](#records): Fetching and persistence methods for your custom structs and class hierarchies. +- [Query Interface](#the-query-interface): A swift way to generate SQL • [table creation](#database-schema) • [requests](#requests) + +**Application Tools** + +- [Migrations](#migrations): Transform your database as your application evolves. +- [Full-Text Search](#full-text-search): Perform efficient and customizable full-text searches. +- [Database Changes Observation](#database-changes-observation): Perform post-commit and post-rollback actions. +- [FetchedRecordsController](#fetchedrecordscontroller): Automated tracking of changes in a query results, plus UITableView animations. +- [Encryption](#encryption): Encrypt your database with SQLCipher. +- [Backup](#backup): Dump the content of a database to another. +- [GRDB Extension Guide](Documentation/ExtendingGRDB.md): When a feature is lacking, extend GRDB right from your application. + +**Good to Know** + +- [Avoiding SQL Injection](#avoiding-sql-injection) +- [Error Handling](#error-handling) +- [Unicode](#unicode) +- [Memory Management](#memory-management) +- [Data Protection](#data-protection) +- [Concurrency](#concurrency) +- [Performance](#performance) + +[FAQ](#faq) + +[Sample Code](#sample-code) + + +Installation +============ + +**The installation procedures below have GRDB use the version of SQLite that ships with the target operating system.** + +See [Encryption](#encryption) for the installation procedure of GRDB with SQLCipher. + +See [Custom SQLite builds](Documentation/CustomSQLiteBuilds.md) for the installation procedure of GRDB with a customized build of SQLite 3.20.0. + + +## CocoaPods + +[CocoaPods](http://cocoapods.org/) is a dependency manager for Xcode projects. To use GRDB with CocoaPods (version 1.2 or higher), specify in your `Podfile`: + +```ruby +use_frameworks! +pod 'GRDB.swift' +``` + + +## Swift Package Manager + +The [Swift Package Manager](https://swift.org/package-manager/) automates the distribution of Swift code. To use GRDB with SPM, add a dependency to your `Package.swift` file: + +```swift +let package = Package( + ... + dependencies: [ + .Package(url: "https://github.com/groue/GRDB.swift.git", majorVersion: 0) + ] +) +``` + +Note that Linux is not currently supported. + + +## Carthage + +Carthage does not support the variety of frameworks built by GRDB (standard SQLite, custom SQLite, SQLCipher). + +Any pull request that has the `make test_CarthageBuild` command successfully complete will be greatly appreciated, though. Bring your local Xcode guru! + + +## Manually + +1. [Download](https://github.com/groue/GRDB.swift/releases/tag/v2.0) a copy of GRDB, or clone its repository and make sure you use the latest tagged version with the `git checkout v2.0` command. + +2. Embed the `GRDB.xcodeproj` project in your own project. + +3. Add the `GRDBOSX`, `GRDBiOS`, or `GRDBWatchOS` target in the **Target Dependencies** section of the **Build Phases** tab of your application target (extension target for WatchOS). + +4. Add the `GRDB.framework` from the targetted platform to the **Embedded Binaries** section of the **General** tab of your application target (extension target for WatchOS). + +See [GRDBDemoiOS](DemoApps/GRDBDemoiOS/GRDBDemoiOS) for an example of such integration. + + +Database Connections +==================== + +GRDB provides two classes for accessing SQLite databases: `DatabaseQueue` and `DatabasePool`: + +```swift +import GRDB + +// Pick one: +let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite") +let dbPool = try DatabasePool(path: "/path/to/database.sqlite") +``` + +The differences are: + +- Database pools allow concurrent database accesses (this can improve the performance of multithreaded applications). +- Unless read-only, database pools open your SQLite database in the [WAL mode](https://www.sqlite.org/wal.html). +- Database queues support [in-memory databases](https://www.sqlite.org/inmemorydb.html). + +**If you are not sure, choose DatabaseQueue.** You will always be able to switch to DatabasePool later. + +- [Database Queues](#database-queues) +- [Database Pools](#database-pools) + + +## Database Queues + +**Open a database queue** with the path to a database file: + +```swift +import GRDB + +let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite") +let inMemoryDBQueue = DatabaseQueue() +``` + +SQLite creates the database file if it does not already exist. The connection is closed when the database queue gets deallocated. + + +**A database queue can be used from any thread.** The `inDatabase` and `inTransaction` methods are synchronous, and block the current thread until your database statements are executed in a protected dispatch queue. They safely serialize the database accesses: + +```swift +// Execute database statements: +try dbQueue.inDatabase { db in + try db.create(table: "places") { ... } + try Place(...).insert(db) +} + +// Wrap database statements in a transaction: +try dbQueue.inTransaction { db in + if let place = try Place.fetchOne(db, key: 1) { + try place.delete(db) + } + return .commit +} + +// Read values: +try dbQueue.inDatabase { db in + let places = try Place.fetchAll(db) + let placeCount = try Place.fetchCount(db) +} + +// Extract a value from the database: +let placeCount = try dbQueue.inDatabase { db in + try Place.fetchCount(db) +} +``` + +**A database queue needs your application to follow rules in order to deliver its safety guarantees.** Please refer to the [Concurrency](#concurrency) chapter. + +See [DemoApps/GRDBDemoiOS/Database.swift](DemoApps/GRDBDemoiOS/GRDBDemoiOS/Database.swift) for a sample code that sets up a database queue on iOS. + + +### DatabaseQueue Configuration + +```swift +var config = Configuration() +config.readonly = true +config.foreignKeysEnabled = true // Default is already true +config.trace = { print($0) } // Prints all SQL statements + +let dbQueue = try DatabaseQueue( + path: "/path/to/database.sqlite", + configuration: config) +``` + +See [Configuration](http://groue.github.io/GRDB.swift/docs/2.0/Structs/Configuration.html) for more details. + + +## Database Pools + +**A Database Pool allows concurrent database accesses.** + +When more efficient than [database queues](#database-queues), database pools also require a good mastery of database transactions. Details follow. If you don't feel comfortable with transactions, use a [database queue](#database-queues) instead. + +```swift +import GRDB +let dbPool = try DatabasePool(path: "/path/to/database.sqlite") +``` + +SQLite creates the database file if it does not already exist. The connection is closed when the database pool gets deallocated. + +> :point_up: **Note**: unless read-only, a database pool opens your database in the SQLite "WAL mode". The WAL mode does not fit all situations. Please have a look at https://www.sqlite.org/wal.html. + + +**A database pool can be used from any thread.** The `read`, `write` and `writeInTransaction` methods are synchronous, and block the current thread until your database statements are executed in a protected dispatch queue. They safely isolate the database accesses: + +```swift +// Execute database statements: +try dbPool.write { db in + try db.create(table: "places") { ... } + try Place(...).insert(db) +} + +// Wrap database statements in a transaction: +try dbPool.writeInTransaction { db in + if let place = try Place.fetchOne(db, key: 1) { + try place.delete(db) + } + return .commit +} + +// Read values: +try dbPool.read { db in + let places = try Place.fetchAll(db) + let placeCount = try Place.fetchCount(db) +} + +// Extract a value from the database: +let placeCount = try dbPool.read { db in + try Place.fetchCount(db) +} +``` + +Database pools allow several threads to access the database at the same time: + +- When you don't need to modify the database, prefer the `read` method, because several threads can perform reads in parallel. + + Reads are generally non-blocking, unless the maximum number of concurrent reads has been reached. In this case, a read has to wait for another read to complete. That maximum number can be [configured](#databasepool-configuration). + +- Unlike reads, writes are serialized. There is never more than a single thread that is writing into the database. + +- Reads are guaranteed an immutable view of the last committed state of the database, regardless of concurrent writes. This kind of isolation is called "snapshot isolation". + + To provide `read` closures an immutable view of the last executed writing block *as a whole*, use `writeInTransaction` instead of `write`. + +**A database pool needs your application to follow rules in order to deliver its safety guarantees.** Please refer to the [Concurrency](#concurrency) chapter. + +See [Advanced DatabasePool](#advanced-databasepool) for more DatabasePool hotness. + +For a sample code that sets up a database pool on iOS, see [DemoApps/GRDBDemoiOS/Database.swift](DemoApps/GRDBDemoiOS/GRDBDemoiOS/Database.swift), and replace DatabaseQueue with DatabasePool. + + +### DatabasePool Configuration + +```swift +var config = Configuration() +config.readonly = true +config.foreignKeysEnabled = true // Default is already true +config.trace = { print($0) } // Prints all SQL statements +config.maximumReaderCount = 10 // The default is 5 + +let dbPool = try DatabasePool( + path: "/path/to/database.sqlite", + configuration: config) +``` + +See [Configuration](http://groue.github.io/GRDB.swift/docs/2.0/Structs/Configuration.html) for more details. + + +Database pools are more memory-hungry than database queues. See [Memory Management](#memory-management) for more information. + + +SQLite API +========== + +**In this section of the documentation, we will talk SQL.** Jump to the [query interface](#the-query-interface) if SQL is not your cup of tea. + +- [Executing Updates](#executing-updates) +- [Fetch Queries](#fetch-queries) + - [Fetching Methods](#fetching-methods) + - [Row Queries](#row-queries) + - [Value Queries](#value-queries) +- [Values](#values) + - [Data](#data-and-memory-savings) + - [Date and DateComponents](#date-and-datecomponents) + - [NSNumber and NSDecimalNumber](#nsnumber-and-nsdecimalnumber) + - [Swift enums](#swift-enums) +- [Transactions and Savepoints](#transactions-and-savepoints) + +Advanced topics: + +- [Custom Value Types](#custom-value-types) +- [Prepared Statements](#prepared-statements) +- [Custom SQL Functions and Aggregates](#custom-sql-functions-and-aggregates) +- [Database Schema Introspection](#database-schema-introspection) +- [Row Adapters](#row-adapters) +- [Raw SQLite Pointers](#raw-sqlite-pointers) + + +## Executing Updates + +Once granted with a [database connection](#database-connections), the `execute` method executes the SQL statements that do not return any database row, such as `CREATE TABLE`, `INSERT`, `DELETE`, `ALTER`, etc. + +For example: + +```swift +try dbQueue.inDatabase { db in + try db.execute(""" + CREATE TABLE players ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + score INT) + """) + + try db.execute( + "INSERT INTO players (name, score) VALUES (:name, :score)", + arguments: ["name": "Barbara", "score": 1000]) + + // Join multiple statements with a semicolon: + try db.execute(""" + INSERT INTO players (name, score) VALUES (?, ?); + INSERT INTO players (name, score) VALUES (?, ?) + """, arguments: ["Arthur", 750, "Barbara", 1000]) +} +``` + +The `?` and colon-prefixed keys like `:name` in the SQL query are the **statements arguments**. You pass arguments with arrays or dictionaries, as in the example above. See [Values](#values) for more information on supported arguments types (Bool, Int, String, Date, Swift enums, etc.). + +Never ever embed values directly in your SQL strings, and always use arguments instead. See [Avoiding SQL Injection](#avoiding-sql-injection) for more information. + +**After an INSERT statement**, you can get the row ID of the inserted row: + +```swift +try db.execute( + "INSERT INTO players (name, score) VALUES (?, ?)", + arguments: ["Arthur", 1000]) +let playerId = db.lastInsertedRowID +``` + +Don't miss [Records](#records), that provide classic **persistence methods**: + +```swift +let player = Player(name: "Arthur", score: 1000) +try player.insert(db) +let playerId = player.id +``` + + +## Fetch Queries + +[Database connections](#database-connections) let you fetch database rows, plain values, and custom models aka "records". + +**Rows** are the raw results of SQL queries: + +```swift +try dbQueue.inDatabase { db in + if let row = try Row.fetchOne(db, "SELECT * FROM wines WHERE id = ?", arguments: [1]) { + let name: String = row["name"] + let color: Color = row["color"] + print(name, color) + } +} +``` + + +**Values** are the Bool, Int, String, Date, Swift enums, etc. stored in row columns: + +```swift +try dbQueue.inDatabase { db in + let urls = try URL.fetchCursor(db, "SELECT url FROM wines") + while let url = try urls.next() { + print(url) + } +} +``` + + +**Records** are your application objects that can initialize themselves from rows: + +```swift +let wines = try dbQueue.inDatabase { db in + try Wine.fetchAll(db, "SELECT * FROM wines") +} +``` + +- [Fetching Methods](#fetching-methods) and [Cursors](#cursors) +- [Row Queries](#row-queries) +- [Value Queries](#value-queries) +- [Records](#records) + + +### Fetching Methods + +**Throughout GRDB**, you can always fetch *cursors*, *arrays*, or *single values* of any fetchable type (database [row](#row-queries), simple [value](#value-queries), or custom [record](#records)): + +```swift +try Row.fetchCursor(...) // A Cursor of Row +try Row.fetchAll(...) // [Row] +try Row.fetchOne(...) // Row? +``` + +- `fetchCursor` returns a **[cursor](#cursors)** over fetched values: + + ```swift + let rows = try Row.fetchCursor(db, "SELECT ...") // A Cursor of Row + ``` + +- `fetchAll` returns an **array**: + + ```swift + let players = try Player.fetchAll(db, "SELECT ...") // [Player] + ``` + +- `fetchOne` returns a **single optional value**, and consumes a single database row (if any). + + ```swift + let count = try Int.fetchOne(db, "SELECT COUNT(*) ...") // Int? + ``` + + +#### Cursors + +**Whenever you consume several rows from the database, you can fetch a Cursor, or an Array**. + +The `fetchAll()` method returns a regular Swift array, that you iterate like all other arrays: + +```swift +try dbQueue.inDatabase { db in + // [Player] + let players = try Player.fetchAll(db, "SELECT ...") + for player in players { + // use player + } +} +``` + +Unlike arrays, cursors returned by `fetchCursor()` load their results step after step: + +```swift +try dbQueue.inDatabase { db in + // Cursor of Player + let players = try Player.fetchCursor(db, "SELECT ...") + while let player = try players.next() { + // use player + } +} +``` + +Both arrays and cursors can iterate over database results. How do you choose one or the other? Look at the differences: + +- Arrays may be consumed on any thread. +- Arrays contain copies of database values. They can take a lot of memory, when there are many fetched results. +- Arrays can be iterated many times. +- Cursors can not be used on any thread: you must consume them in a protected database queue. +- Cursors iterate database results in a lazy fashion, and don't consume much memory. +- Cursors can be iterated only one time. +- Cursors are granted with direct access to SQLite: you can especially expect the best performance from cursors of raw database rows and some primitive types like `Int`, `String`, or `Bool` that adopt the [StatementColumnConvertible](http://groue.github.io/GRDB.swift/docs/2.0/Protocols/StatementColumnConvertible.html) protocol. + +If you don't see, or don't care about the difference, use arrays. If you care about memory and performance, use cursors when appropriate. + +**There are several cursor types**, depending on the type of fetched values (database [row](#row-queries), simple [value](#value-queries), or custom [record](#records)): + +```swift +Row.fetchCursor(...) // RowCursor +Int.fetchCursor(...) // ColumnCursor<Int> +Date.fetchCursor(...) // DatabaseValueCursor<Date> +Player.fetchCursor(...) // RecordCursor<Player> +``` + +All cursor types adopt the [Cursor](http://groue.github.io/GRDB.swift/docs/2.0/Protocols/Cursor.html) protocol, which looks a lot like standard [lazy sequences](https://developer.apple.com/reference/swift/lazysequenceprotocol) of Swift. As such, cursors come with many methods: `contains`, `enumerated`, `filter`, `first`, `flatMap`, `forEach`, `joined`, `map`, `reduce`: + +```swift +// Iterate all Github links +try URL + .fetchCursor(db, "SELECT url FROM links") + .filter { url in url.host == "github.com" } + .forEach { url in ... } + +// Turn a cursor into an array: +let cursor = URL + .fetchCursor(db, "SELECT url FROM links") + .filter { url in url.host == "github.com" } +let githubURLs = try Array(cursor) // [URL] +``` + +> :point_up: Don't modify the fetched results during a cursor iteration: +> +> ```swift +> // Undefined behavior +> while let place = try places.next() { +> try db.execute("DELETE ...") +> } +> ``` +> +> :point_up: **Don't turn a cursor of `Row` into an array**. You would not get the distinct rows you expect. To get a array of rows, use `Row.fetchAll(...)`. Generally speaking, make sure you copy a row whenever you extract it from a cursor for later use: `row.copy()`. + + +### Row Queries + +- [Fetching Rows](#fetching-rows) +- [Column Values](#column-values) +- [DatabaseValue](#databasevalue) +- [Rows as Dictionaries](#rows-as-dictionaries) + + +#### Fetching Rows + +Fetch **cursors** of rows, **arrays**, or **single** rows (see [fetching methods](#fetching-methods)): + +```swift +try dbQueue.inDatabase { db in + try Row.fetchCursor(db, "SELECT ...", arguments: ...) // A Cursor of Row + try Row.fetchAll(db, "SELECT ...", arguments: ...) // [Row] + try Row.fetchOne(db, "SELECT ...", arguments: ...) // Row? + + let rows = try Row.fetchCursor(db, "SELECT * FROM wines") + while let row = try rows.next() { + let name: String = row["name"] + let color: Color = row["color"] + print(name, color) + } +} + +let rows = try dbQueue.inDatabase { db in + try Row.fetchAll(db, "SELECT * FROM players") +} +``` + +Arguments are optional arrays or dictionaries that fill the positional `?` and colon-prefixed keys like `:name` in the query: + +```swift +let rows = try Row.fetchAll(db, + "SELECT * FROM players WHERE name = ?", + arguments: ["Arthur"]) + +let rows = try Row.fetchAll(db, + "SELECT * FROM players WHERE name = :name", + arguments: ["name": "Arthur"]) +``` + +See [Values](#values) for more information on supported arguments types (Bool, Int, String, Date, Swift enums, etc.), and [StatementArguments](http://groue.github.io/GRDB.swift/docs/2.0/Structs/StatementArguments.html) for a detailed documentation of SQLite arguments. + +Unlike row arrays that contain copies of the database rows, row cursors are close to the SQLite metal, and require a little care: + +> :point_up: **Don't turn a cursor of `Row` into an array**. You would not get the distinct rows you expect. To get a array of rows, use `Row.fetchAll(...)`. Generally speaking, make sure you copy a row whenever you extract it from a cursor for later use: `row.copy()`. + + +#### Column Values + +**Read column values** by index or column name: + +```swift +let name: String = row[0] // 0 is the leftmost column +let name: String = row["name"] // Leftmost matching column - lookup is case-insensitive +let name: String = row[Column("name")] // Using query interface's Column +``` + +Make sure to ask for an optional when the value may be NULL: + +```swift +let name: String? = row["name"] +``` + +The `row[]` subscript returns the type you ask for. See [Values](#values) for more information on supported value types: + +```swift +let bookCount: Int = row["bookCount"] +let bookCount64: Int64 = row["bookCount"] +let hasBooks: Bool = row["bookCount"] // false when 0 + +let string: String = row["date"] // "2015-09-11 18:14:15.123" +let date: Date = row["date"] // Date +self.date = row["date"] // Depends on the type of the property. +``` + +You can also use the `as` type casting operator: + +```swift +row[...] as Int +row[...] as Int? +``` + +> :warning: **Warning**: avoid the `as!` and `as?` operators, because they misbehave in the context of type inference (see [rdar://21676393](http://openradar.appspot.com/radar?id=4951414862249984)): +> +> ```swift +> if let int = row[...] as? Int { ... } // BAD - doesn't work +> if let int = row[...] as Int? { ... } // GOOD +> ``` + +Generally speaking, you can extract the type you need, *provided it can be converted from the underlying SQLite value*: + +- **Successful conversions include:** + + - All numeric SQLite values to all numeric Swift types, and Bool (zero is the only false boolean). + - Text SQLite values to Swift String. + - Blob SQLite values to Foundation Data. + + See [Values](#values) for more information on supported types (Bool, Int, String, Date, Swift enums, etc.) + +- **NULL returns nil.** + + ```swift + let row = try Row.fetchOne(db, "SELECT NULL")! + row[0] as Int? // nil + row[0] as Int // fatal error: could not convert NULL to Int. + ``` + + There is one exception, though: the [DatabaseValue](#databasevalue) type: + + ```swift + row[0] as DatabaseValue // DatabaseValue.null + ``` + +- **Missing columns return nil.** + + ```swift + let row = try Row.fetchOne(db, "SELECT 'foo' AS foo")! + row["missing"] as String? // nil + row["missing"] as String // fatal error: no such column: missing + ``` + + You can explicitly check for a column presence with the `hasColumn` method. + +- **Invalid conversions throw a fatal error.** + + ```swift + let row = try Row.fetchOne(db, "SELECT 'Mom’s birthday'")! + row[0] as String // "Mom’s birthday" + row[0] as Date? // fatal error: could not convert "Mom’s birthday" to Date. + row[0] as Date // fatal error: could not convert "Mom’s birthday" to Date. + ``` + + This fatal error can be avoided: see [Fatal Errors](#fatal-errors). + +- **SQLite has a weak type system, and provides [convenience conversions](https://www.sqlite.org/c3ref/column_blob.html) that can turn Blob to String, String to Int, etc.** + + GRDB will sometimes let those conversions go through: + + ```swift + let rows = try Row.fetchCursor(db, "SELECT '20 small cigars'") + while let row = try rows.next() { + row[0] as Int // 20 + } + ``` + + Don't freak out: those conversions did not prevent SQLite from becoming the immensely successful database engine you want to use. And GRDB adds safety checks described just above. You can also prevent those convenience conversions altogether by using the [DatabaseValue](#databasevalue) type. + + +#### DatabaseValue + +**DatabaseValue is an intermediate type between SQLite and your values, which gives information about the raw value stored in the database.** + +You get DatabaseValue just like other value types: + +```swift +let dbValue: DatabaseValue = row[0] +let dbValue: DatabaseValue = row["name"] + +// Check for NULL: +dbValue.isNull // Bool + +// The stored value: +dbValue.storage.value // Int64, Double, String, Data, or nil + +// All the five storage classes supported by SQLite: +switch dbValue.storage { +case .null: print("NULL") +case .int64(let int64): print("Int64: \(int64)") +case .double(let double): print("Double: \(double)") +case .string(let string): print("String: \(string)") +case .blob(let data): print("Data: \(data)") +} +``` + +You can extract regular [values](#values) (Bool, Int, String, Date, Swift enums, etc.) from DatabaseValue with the [DatabaseValueConvertible.fromDatabaseValue()](#custom-value-types) method: + +```swift +let dbValue: DatabaseValue = row["bookCount"] +let bookCount = Int.fromDatabaseValue(dbValue) // Int? +let bookCount64 = Int64.fromDatabaseValue(dbValue) // Int64? +let hasBooks = Bool.fromDatabaseValue(dbValue) // Bool?, false when 0 + +let dbValue: DatabaseValue = row["date"] +let string = String.fromDatabaseValue(dbValue) // "2015-09-11 18:14:15.123" +let date = Date.fromDatabaseValue(dbValue) // Date? +``` + +`fromDatabaseValue` returns nil for invalid conversions: + +```swift +let row = try Row.fetchOne(db, "SELECT 'Mom’s birthday'")! +let dbValue: DatabaseValue = row[0] +let string = String.fromDatabaseValue(dbValue) // "Mom’s birthday" +let int = Int.fromDatabaseValue(dbValue) // nil +let date = Date.fromDatabaseValue(dbValue) // nil +``` + +This turns out useful when you process untrusted databases. Compare: + +```swift +let date: Date? = row[0] // fatal error: could not convert "Mom’s birthday" to Date. +let date = Date.fromDatabaseValue(row[0]) // nil +``` + + +#### Rows as Dictionaries + +Row adopts the standard [Collection](https://developer.apple.com/reference/swift/collection) protocol, and can be seen as a dictionary of [DatabaseValue](#databasevalue): + +```swift +// All the (columnName, dbValue) tuples, from left to right: +for (columnName, dbValue) in row { + ... +} +``` + +**You can build rows from dictionaries** (standard Swift dictionaries and NSDictionary). See [Values](#values) for more information on supported types: + +```swift +let row: Row = ["name": "foo", "date": nil] +let row = Row(["name": "foo", "date": nil]) +let row = Row(/* [AnyHashable: Any] */) // nil if invalid dictionary +``` + +Yet rows are not real dictionaries: they may contain duplicate columns: + +```swift +let row = try Row.fetchOne(db, "SELECT 1 AS foo, 2 AS foo")! +row.columnNames // ["foo", "foo"] +row.databaseValues // [1, 2] +row["foo"] // 1 (leftmost matching column) +for (columnName, dbValue) in row { ... } // ("foo", 1), ("foo", 2) +``` + +**When you build a dictionary from a row**, you have to disambiguate identical columns, and choose how to present database values. For example: + +- A `[String: DatabaseValue]` dictionary that keeps leftmost value in case of duplicated column name: + + ```swift + let dict = Dictionary(row, uniquingKeysWith: { $0 }) + ``` + +- A `[String: AnyObject]` dictionary which keeps rightmost value in case of duplicated column name. This dictionary is identical to FMResultSet's resultDictionary from FMDB. It contains NSNull values for null columns, and can be shared with Objective-C: + + ```swift + let dict = Dictionary( + row.map { (column, dbValue) in + (column, dbValue.storage.value as AnyObject) + }, + uniquingKeysWith: { $1 }) + ``` + +- A `[String: Any]` dictionary that can feed, for example, JSONSerialization: + + ```swift + let dict = Dictionary( + row.map { (column, dbValue) in + (column, dbValue.storage.value) + }, + uniquingKeysWith: { $0 }) + ``` + +See the documentation of [`Dictionary.init(_:uniquingKeysWith:)`](https://developer.apple.com/documentation/swift/dictionary/2892961-init) for more information. + + +### Value Queries + +Instead of rows, you can directly fetch **[values](#values)**. Like rows, fetch them as **cursors**, **arrays**, or **single** values (see [fetching methods](#fetching-methods)). Values are extracted from the leftmost column of the SQL queries: + +```swift +try dbQueue.inDatabase { db in + try Int.fetchCursor(db, "SELECT ...", arguments: ...) // A Cursor of Int + try Int.fetchAll(db, "SELECT ...", arguments: ...) // [Int] + try Int.fetchOne(db, "SELECT ...", arguments: ...) // Int? + + // When database may contain NULL: + try Optional<Int>.fetchCursor(db, "SELECT ...", arguments: ...) // A Cursor of Int? + try Optional<Int>.fetchAll(db, "SELECT ...", arguments: ...) // [Int?] +} + +let playerCount = try dbQueue.inDatabase { db in + try Int.fetchOne(db, "SELECT COUNT(*) FROM players")! +} +``` + +`fetchOne` returns an optional value which is nil in two cases: either the SELECT statement yielded no row, or one row with a NULL value. + +There are many supported value types (Bool, Int, String, Date, Swift enums, etc.). See [Values](#values) for more information: + +```swift +let count = try Int.fetchOne(db, "SELECT COUNT(*) FROM players")! // Int +let urls = try URL.fetchAll(db, "SELECT url FROM links") // [URL] +``` + + +## Values + +GRDB ships with built-in support for the following value types: + +- **Swift Standard Library**: Bool, Double, Float, all signed and unsigned integer types, String, [Swift enums](#swift-enums). + +- **Foundation**: [Data](#data-and-memory-savings), [Date](#date-and-datecomponents), [DateComponents](#date-and-datecomponents), NSNull, [NSNumber](#nsnumber-and-nsdecimalnumber), NSString, URL, [UUID](#uuid). + +- **CoreGraphics**: CGFloat. + +- **[DatabaseValue](#databasevalue)**, the type which gives information about the raw value stored in the database. + +- **Full-Text Patterns**: [FTS3Pattern](#fts3pattern) and [FTS5Pattern](#fts5pattern). + +- Generally speaking, all types that adopt the [DatabaseValueConvertible](#custom-value-types) protocol. + +Values can be used as [statement arguments](http://groue.github.io/GRDB.swift/docs/2.0/Structs/StatementArguments.html): + +```swift +let url: URL = ... +let verified: Bool = ... +try db.execute( + "INSERT INTO links (url, verified) VALUES (?, ?)", + arguments: [url, verified]) +``` + +Values can be [extracted from rows](#column-values): + +```swift +let rows = try Row.fetchCursor(db, "SELECT * FROM links") +while let row = try rows.next() { + let url: URL = row["url"] + let verified: Bool = row["verified"] +} +``` + +Values can be [directly fetched](#value-queries): + +```swift +let urls = try URL.fetchAll(db, "SELECT url FROM links") // [URL] +``` + +Use values in [Records](#records): + +```swift +class Link : Record { + var url: URL + var isVerified: Bool + + required init(row: Row) { + url = row["url"] + isVerified = row["verified"] + super.init(row: row) + } + + override func encode(to container: inout PersistenceContainer) { + container["url"] = url + container["verified"] = isVerified + } +} +``` + +Use values in the [query interface](#the-query-interface): + +```swift +let url: URL = ... +let link = try Link.filter(urlColumn == url).fetchOne(db) +``` + + +### Data (and Memory Savings) + +**Data** suits the BLOB SQLite columns. It can be stored and fetched from the database just like other [values](#values): + +```swift +let rows = try Row.fetchCursor(db, "SELECT data, ...") +while let row = try rows.next() { + let data: Data = row["data"] +} +``` + +At each step of the request iteration, the `row[]` subscript creates *two copies* of the database bytes: one fetched by SQLite, and another, stored in the Swift Data value. + +**You have the opportunity to save memory** by not copying the data fetched by SQLite: + +```swift +while let row = try rows.next() { + let data = row.dataNoCopy(named: "data") // Data? +} +``` + +The non-copied data does not live longer than the iteration step: make sure that you do not use it past this point. + + +### Date and DateComponents + +[**Date**](#date) and [**DateComponents**](#datecomponents) can be stored and fetched from the database. + +Here is the support provided by GRDB for the various [date formats](https://www.sqlite.org/lang_datefunc.html) supported by SQLite: + +| SQLite format | Date | DateComponents | +|:---------------------------- |:------------:|:--------------:| +| YYYY-MM-DD | Read ¹ | Read/Write | +| YYYY-MM-DD HH:MM | Read ¹ | Read/Write | +| YYYY-MM-DD HH:MM:SS | Read ¹ | Read/Write | +| YYYY-MM-DD HH:MM:SS.SSS | Read/Write ¹ | Read/Write | +| YYYY-MM-DD**T**HH:MM | Read ¹ | Read | +| YYYY-MM-DD**T**HH:MM:SS | Read ¹ | Read | +| YYYY-MM-DD**T**HH:MM:SS.SSS | Read ¹ | Read | +| HH:MM | | Read/Write | +| HH:MM:SS | | Read/Write | +| HH:MM:SS.SSS | | Read/Write | +| Timestamps since unix epoch | Read ² | | +| `now` | | | + +¹ Dates are stored and read in the UTC time zone. Missing components are assumed to be zero. + +² GRDB 2.0 interprets numerical values as timestamps that fuel `Date(timeIntervalSince1970:)`. Previous GRDB versions used to interpret numbers as [julian days](https://en.wikipedia.org/wiki/Julian_day). GRDB 2.0 still supports julian days, with the `Date(julianDay:)` initializer. + + +#### Date + +**Date** can be stored and fetched from the database just like other [values](#values): + +```swift +try db.execute( + "INSERT INTO players (creationDate, ...) VALUES (?, ...)", + arguments: [Date(), ...]) + +let creationDate: Date = row["creationDate"] +``` + +Dates are stored using the format "YYYY-MM-DD HH:MM:SS.SSS" in the UTC time zone. It is precise to the millisecond. + +> :point_up: **Note**: this format was chosen because it is the only format that is: +> +> - Comparable (`ORDER BY date` works) +> - Comparable with the SQLite keyword CURRENT_TIMESTAMP (`WHERE date > CURRENT_TIMESTAMP` works) +> - Able to feed [SQLite date & time functions](https://www.sqlite.org/lang_datefunc.html) +> - Precise enough +> +> Yet this format may not fit your needs. For example, you may want to store dates as timestamps. In this case, store and load Doubles instead of Date, and perform the required conversions. + + +#### DateComponents + +DateComponents is indirectly supported, through the **DatabaseDateComponents** helper type. + +DatabaseDateComponents reads date components from all [date formats supported by SQLite](https://www.sqlite.org/lang_datefunc.html), and stores them in the format of your choice, from HH:MM to YYYY-MM-DD HH:MM:SS.SSS. + +DatabaseDateComponents can be stored and fetched from the database just like other [values](#values): + +```swift +let components = DateComponents() +components.year = 1973 +components.month = 9 +components.day = 18 + +// Store "1973-09-18" +let dbComponents = DatabaseDateComponents(components, format: .YMD) +try db.execute( + "INSERT INTO players (birthDate, ...) VALUES (?, ...)", + arguments: [dbComponents, ...]) + +// Read "1973-09-18" +let row = try Row.fetchOne(db, "SELECT birthDate ...")! +let dbComponents: DatabaseDateComponents = row["birthDate"] +dbComponents.format // .YMD (the actual format found in the database) +dbComponents.dateComponents // DateComponents +``` + + +### NSNumber and NSDecimalNumber + +**NSNumber** can be stored and fetched from the database just like other [values](#values). Floating point NSNumbers are stored as Double. Integer and boolean, as Int64. Integers that don't fit Int64 won't be stored: you'll get a fatal error instead. Be cautious when an NSNumber contains an UInt64, for example. + +NSDecimalNumber deserves a longer discussion: + +**SQLite has no support for decimal numbers.** Given the table below, SQLite will actually store integers or doubles: + +```sql +CREATE TABLE transfers ( + amount DECIMAL(10,5) -- will store integer or double, actually +) +``` + +This means that computations will not be exact: + +```swift +try db.execute("INSERT INTO transfers (amount) VALUES (0.1)") +try db.execute("INSERT INTO transfers (amount) VALUES (0.2)") +let sum = try NSDecimalNumber.fetchOne(db, "SELECT SUM(amount) FROM transfers")! + +// Yikes! 0.3000000000000000512 +print(sum) +``` + +Don't blame SQLite or GRDB, and instead store your decimal numbers differently. + +A classic technique is to store *integers* instead, since SQLite performs exact computations of integers. For example, don't store Euros, but store cents instead: + +```swift +// Store +let amount = NSDecimalNumber(string: "0.1") // 0.1 +let integerAmount = amount.multiplying(byPowerOf10: 2).int64Value // 100 +try db.execute("INSERT INTO transfers (amount) VALUES (?)", arguments: [integerAmount]) + +// Read +let integerAmount = try Int64.fetchOne(db, "SELECT SUM(amount) FROM transfers")! // 100 +let amount = NSDecimalNumber(value: integerAmount).multiplying(byPowerOf10: -2) // 0.1 +``` + + +### UUID + +**UUID** can be stored and fetched from the database just like other [values](#values). GRDB stores uuids as 16-bytes data blobs. + + +### Swift Enums + +**Swift enums** and generally all types that adopt the [RawRepresentable](https://developer.apple.com/library/tvos/documentation/Swift/Reference/Swift_RawRepresentable_Protocol/index.html) protocol can be stored and fetched from the database just like their raw [values](#values): + +```swift +enum Color : Int { + case red, white, rose +} + +enum Grape : String { + case chardonnay, merlot, riesling +} + +// Declare empty DatabaseValueConvertible adoption +extension Color : DatabaseValueConvertible { } +extension Grape : DatabaseValueConvertible { } + +// Store +try db.execute( + "INSERT INTO wines (grape, color) VALUES (?, ?)", + arguments: [Grape.merlot, Color.red]) + +// Read +let rows = try Row.fetchCursor(db, "SELECT * FROM wines") +while let row = try rows.next() { + let grape: Grape = row["grape"] + let color: Color = row["color"] +} +``` + +**When a database value does not match any enum case**, you get a fatal error. This fatal error can be avoided with the [DatabaseValueConvertible.fromDatabaseValue()](#custom-value-types) method: + +```swift +let row = try Row.fetchOne(db, "SELECT 'syrah'")! + +row[0] as String // "syrah" +row[0] as Grape? // fatal error: could not convert "syrah" to Grape. +row[0] as Grape // fatal error: could not convert "syrah" to Grape. +Grape.fromDatabaseValue(row[0]) // nil +``` + + +## Transactions and Savepoints + +The `DatabaseQueue.inTransaction()` and `DatabasePool.writeInTransaction()` methods open an SQLite transaction and run their closure argument in a protected dispatch queue. They block the current thread until your database statements are executed: + +```swift +try dbQueue.inTransaction { db in + let wine = Wine(color: .red, name: "Pomerol") + try wine.insert(db) + return .commit +} +``` + +If an error is thrown within the transaction body, the transaction is rollbacked and the error is rethrown by the `inTransaction` method. If you return `.rollback` from your closure, the transaction is also rollbacked, but no error is thrown. + +If you want to insert a transaction between other database statements, you can use the Database.inTransaction() function, or even raw SQL statements: + +```swift +try dbQueue.inDatabase { db in // or dbPool.write { db in + ... + try db.inTransaction { + ... + return .commit + } + ... + try db.execute("BEGIN TRANSACTION") + ... + try db.execute("COMMIT") +} +``` + +You can ask a database if a transaction is currently opened: + +```swift +func myCriticalMethod(_ db: Database) throws { + precondition(db.isInsideTransaction, "This method requires a transaction") + try ... +} +``` + +Yet, you have a better option than checking for transactions: critical sections of your application should use savepoints, described below: + +```swift +func myCriticalMethod(_ db: Database) throws { + try db.inSavepoint { + // Here the database is guaranteed to be inside a transaction. + try ... + } +} +``` + + +### Savepoints + +**Statements grouped in a savepoint can be rollbacked without invalidating a whole transaction:** + +```swift +try dbQueue.inTransaction { db in + try db.inSavepoint { + try db.execute("DELETE ...") + try db.execute("INSERT ...") // need to rollback the delete above if this fails + return .commit + } + + // Other savepoints, etc... + return .commit +} +``` + +If an error is thrown within the savepoint body, the savepoint is rollbacked and the error is rethrown by the `inSavepoint` method. If you return `.rollback` from your closure, the body is also rollbacked, but no error is thrown. + +**Unlike transactions, savepoints can be nested.** They implicitly open a transaction if no one was opened when the savepoint begins. As such, they behave just like nested transactions. Yet the database changes are only committed to disk when the outermost savepoint is committed: + +```swift +try dbQueue.inDatabase { db in + try db.inSavepoint { + ... + try db.inSavepoint { + ... + return .commit + } + ... + return .commit // writes changes to disk + } +} +``` + +SQLite savepoints are more than nested transactions, though. For advanced savepoints uses, use [SQL queries](https://www.sqlite.org/lang_savepoint.html). + + +### Transaction Kinds + +SQLite supports [three kinds of transactions](https://www.sqlite.org/lang_transaction.html): deferred (the default), immediate, and exclusive. + +The transaction kind can be changed in the database configuration, or for each transaction: + +```swift +// Set the default transaction kind to IMMEDIATE: +var config = Configuration() +config.defaultTransactionKind = .immediate +let dbQueue = try DatabaseQueue(path: "...", configuration: config) + +// BEGIN IMMEDIATE TRANSACTION ... +dbQueue.inTransaction { db in ... } + +// BEGIN EXCLUSIVE TRANSACTION ... +dbQueue.inTransaction(.exclusive) { db in ... } +``` + + +## Custom Value Types + +Conversion to and from the database is based on the `DatabaseValueConvertible` protocol: + +```swift +protocol DatabaseValueConvertible { + /// Returns a value that can be stored in the database. + var databaseValue: DatabaseValue { get } + + /// Returns a value initialized from dbValue, if possible. + static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? +} +``` + +All types that adopt this protocol can be used like all other [values](#values) (Bool, Int, String, Date, Swift enums, etc.) + +The `databaseValue` property returns [DatabaseValue](#databasevalue), a type that wraps the five values supported by SQLite: NULL, Int64, Double, String and Data. Since DatabaseValue has no public initializer, use `DatabaseValue.null`, or another type that already adopts the protocol: `1.databaseValue`, `"foo".databaseValue`, etc. Conversion to DatabaseValue *must not* fail. + +The `fromDatabaseValue()` factory method returns an instance of your custom type if the database value contains a suitable value. If the database value does not contain a suitable value, such as "foo" for Date, `fromDatabaseValue` *must* return nil (GRDB will interpret this nil result as a conversion error, and react accordingly). + +The [GRDB Extension Guide](Documentation/ExtendingGRDB.md) contains sample code that has UIColor adopt DatabaseValueConvertible. + + +## Prepared Statements + +**Prepared Statements** let you prepare an SQL query and execute it later, several times if you need, with different arguments. + +There are two kinds of prepared statements: **select statements**, and **update statements**: + +```swift +try dbQueue.inDatabase { db in + let updateSQL = "INSERT INTO players (name, score) VALUES (:name, :score)" + let updateStatement = try db.makeUpdateStatement(updateSQL) + + let selectSQL = "SELECT * FROM players WHERE name = ?" + let selectStatement = try db.makeSelectStatement(selectSQL) +} +``` + +The `?` and colon-prefixed keys like `:name` in the SQL query are the statement arguments. You set them with arrays or dictionaries (arguments are actually of type [StatementArguments](http://groue.github.io/GRDB.swift/docs/2.0/Structs/StatementArguments.html), which happens to adopt the ExpressibleByArrayLiteral and ExpressibleByDictionaryLiteral protocols). + +```swift +updateStatement.arguments = ["name": "Arthur", "score": 1000] +selectStatement.arguments = ["Arthur"] +``` + +After arguments are set, you can execute the prepared statement: + +```swift +try updateStatement.execute() +``` + +Select statements can be used wherever a raw SQL query string would fit (see [fetch queries](#fetch-queries)): + +```swift +let rows = try Row.fetchCursor(selectStatement) // A Cursor of Row +let players = try Player.fetchAll(selectStatement) // [Player] +let player = try Player.fetchOne(selectStatement) // Player? +``` + +You can set the arguments at the moment of the statement execution: + +```swift +try updateStatement.execute(arguments: ["name": "Arthur", "score": 1000]) +let player = try Player.fetchOne(selectStatement, arguments: ["Arthur"]) +``` + +> :point_up: **Note**: it is a programmer error to reuse a prepared statement that has failed: GRDB may crash if you do so. + +See [row queries](#row-queries), [value queries](#value-queries), and [Records](#records) for more information. + + +### Prepared Statements Cache + +When the same query will be used several times in the lifetime of your application, you may feel a natural desire to cache prepared statements. + +**Don't cache statements yourself.** + +> :point_up: **Note**: This is because you don't have the necessary tools. Statements are tied to specific SQLite connections and dispatch queues which you don't manage yourself, especially when you use [database pools](#database-pools). A change in the database schema [may, or may not](https://www.sqlite.org/compile.html#max_schema_retry) invalidate a statement. On systems earlier than iOS 8.2 and OSX 10.10 that don't have the [sqlite3_close_v2 function](https://www.sqlite.org/c3ref/close.html), SQLite connections won't close properly if statements have been kept alive. + +Instead, use the `cachedUpdateStatement` and `cachedSelectStatement` methods. GRDB does all the hard caching and [memory management](#memory-management) stuff for you: + +```swift +let updateStatement = try db.cachedUpdateStatement(sql) +let selectStatement = try db.cachedSelectStatement(sql) +``` + +Should a cached prepared statement throw an error, don't reuse it (it is a programmer error). Instead, reload it from the cache. + + +## Custom SQL Functions and Aggregates + +**SQLite lets you define SQL functions and aggregates.** + +A custom SQL function or aggregate extends SQLite: + +```sql +SELECT reverse(name) FROM players; -- custom function +SELECT maxLength(name) FROM players; -- custom aggregate +``` + +- [Custom SQL Functions](#custom-sql-functions) +- [Custom Aggregates](#custom-aggregates) + + +### Custom SQL Functions + +```swift +let reverse = DatabaseFunction("reverse", argumentCount: 1, pure: true) { (values: [DatabaseValue]) in + // Extract string value, if any... + guard let string = String.fromDatabaseValue(values[0]) else { + return nil + } + // ... and return reversed string: + return String(string.characters.reversed()) +} +dbQueue.add(function: reverse) // Or dbPool.add(function: ...) + +try dbQueue.inDatabase { db in + // "oof" + try String.fetchOne(db, "SELECT reverse('foo')")! +} +``` + +The *function* argument takes an array of [DatabaseValue](#databasevalue), and returns any valid [value](#values) (Bool, Int, String, Date, Swift enums, etc.) The number of database values is guaranteed to be *argumentCount*. + +SQLite has the opportunity to perform additional optimizations when functions are "pure", which means that their result only depends on their arguments. So make sure to set the *pure* argument to true when possible. + + +**Functions can take a variable number of arguments:** + +When you don't provide any explicit *argumentCount*, the function can take any number of arguments: + +```swift +let averageOf = DatabaseFunction("averageOf", pure: true) { (values: [DatabaseValue]) in + let doubles = values.flatMap { Double.fromDatabaseValue($0) } + return doubles.reduce(0, +) / Double(doubles.count) +} +dbQueue.add(function: averageOf) + +try dbQueue.inDatabase { db in + // 2.0 + try Double.fetchOne(db, "SELECT averageOf(1, 2, 3)")! +} +``` + + +**Functions can throw:** + +```swift +let sqrt = DatabaseFunction("sqrt", argumentCount: 1, pure: true) { (values: [DatabaseValue]) in + guard let double = Double.fromDatabaseValue(values[0]) else { + return nil + } + guard double >= 0 else { + throw DatabaseError(message: "invalid negative number") + } + return sqrt(double) +} +dbQueue.add(function: sqrt) + +// SQLite error 1 with statement `SELECT sqrt(-1)`: invalid negative number +try dbQueue.inDatabase { db in + try Double.fetchOne(db, "SELECT sqrt(-1)")! +} +``` + + +**Use custom functions in the [query interface](#the-query-interface):** + +```swift +// SELECT reverseString("name") FROM players +Player.select(reverseString.apply(nameColumn)) +``` + + +**GRDB ships with built-in SQL functions that perform unicode-aware string transformations.** See [Unicode](#unicode). + + +### Custom Aggregates + +Before registering a custom aggregate, you need to define a type that adopts the `DatabaseAggregate` protocol: + +```swift +protocol DatabaseAggregate { + // Initializes an aggregate + init() + + // Called at each step of the aggregation + mutating func step(_ dbValues: [DatabaseValue]) throws + + // Returns the final result + func finalize() throws -> DatabaseValueConvertible? +} +``` + +For example: + +```swift +struct MaxLength : DatabaseAggregate { + var maxLength: Int = 0 + + mutating func step(_ dbValues: [DatabaseValue]) { + // At each step, extract string value, if any... + guard let string = String.fromDatabaseValue(dbValues[0]) else { + return + } + // ... and update the result + let length = string.characters.count + if length > maxLength { + maxLength = length + } + } + + func finalize() -> DatabaseValueConvertible? { + return maxLength + } +} + +let maxLength = DatabaseFunction( + "maxLength", + argumentCount: 1, + pure: true, + aggregate: MaxLength.self) + +dbQueue.add(function: maxLength) // Or dbPool.add(function: ...) + +try dbQueue.inDatabase { db in + // Some Int + try Int.fetchOne(db, "SELECT maxLength(name) FROM players")! +} +``` + +The `step` method of the aggregate takes an array of [DatabaseValue](#databasevalue). This array contains as many values as the *argumentCount* parameter (or any number of values, when *argumentCount* is omitted). + +The `finalize` method of the aggregate returns the final aggregated [value](#values) (Bool, Int, String, Date, Swift enums, etc.). + +SQLite has the opportunity to perform additional optimizations when aggregates are "pure", which means that their result only depends on their inputs. So make sure to set the *pure* argument to true when possible. + + +**Use custom aggregates in the [query interface](#the-query-interface):** + +```swift +// SELECT maxLength("name") FROM players +Player.select(maxLength.apply(nameColumn)) + .asRequest(of: Int.self) + .fetchOne(db) // Int? +``` + + +## Database Schema Introspection + +**SQLite provides database schema introspection tools**, such as the [sqlite_master](https://www.sqlite.org/faq.html#q7) table, and the pragma [table_info](https://www.sqlite.org/pragma.html#pragma_table_info): + +```swift +try db.create(table: "players") { t in + t.column("id", .integer).primaryKey() + t.column("name", .text) +} + +// <Row type:"table" name:"players" tbl_name:"players" rootpage:2 +// sql:"CREATE TABLE players(id INTEGER PRIMARY KEY, name TEXT)"> +for row in try Row.fetchAll(db, "SELECT * FROM sqlite_master") { + print(row) +} + +// <Row cid:0 name:"id" type:"INTEGER" notnull:0 dflt_value:NULL pk:1> +// <Row cid:1 name:"name" type:"TEXT" notnull:0 dflt_value:NULL pk:0> +for row in try Row.fetchAll(db, "PRAGMA table_info('players')") { + print(row) +} +``` + +GRDB provides high-level methods as well: + +```swift +try db.tableExists("players") // Bool, true if the table exists +try db.columnCount(in: "players") // Int, the number of columns in table +try db.indexes(on: "players") // [IndexInfo], the indexes defined on the table +try db.table("players", hasUniqueKey: ["email"]) // Bool, true if column(s) is a unique key +try db.foreignKeys(on: "players") // [ForeignKeyInfo], the foreign keys defined on the table +try db.primaryKey("players") // PrimaryKeyInfo +``` + + +## Row Adapters + +**Row adapters let you present database rows in the way expected by the row consumers.** + +They basically help two incompatible row interfaces to work together. For example, a row consumer expects a column named "consumed", but the produced row has a column named "produced". + +In this case, the `ColumnMapping` row adapter comes in handy: + +```swift +// Fetch a 'produced' column, and consume a 'consumed' column: +let adapter = ColumnMapping(["consumed": "produced"]) +let row = try Row.fetchOne(db, "SELECT 'Hello' AS produced", adapter: adapter)! +row["consumed"] // "Hello" +row["produced"] // nil +``` + +Row adapters are values that adopt the [RowAdapter](http://groue.github.io/GRDB.swift/docs/2.0/Protocols/RowAdapter.html) protocol. You can implement your own custom adapters ([**:fire: EXPERIMENTAL**](#what-are-experimental-features)), or use one of the four built-in adapters: + + +### ColumnMapping + +ColumnMapping renames columns. Build one with a dictionary whose keys are adapted column names, and values the column names in the raw row: + +```swift +// <Row newName:"Hello"> +let adapter = ColumnMapping(["newName": "oldName"]) +let row = try Row.fetchOne(db, "SELECT 'Hello' AS oldName", adapter: adapter)! +``` + +### SuffixRowAdapter + +`SuffixRowAdapter` hides the first columns in a row: + +```swift +// <Row b:1 c:2> +let adapter = SuffixRowAdapter(fromIndex: 1) +let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)! +``` + +### RangeRowAdapter + +`RangeRowAdapter` only exposes a range of columns. + +```swift +// <Row b:1> +let adapter = RangeRowAdapter(1..<2) +let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)! +``` + +### ScopeAdapter + +`ScopeAdapter` defines *row scopes*: + +```swift +let adapter = ScopeAdapter([ + "left": RangeRowAdapter(0..<2), + "right": RangeRowAdapter(2..<4)]) +let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d", adapter: adapter)! +``` + +ScopeAdapter does not change the columns and values of the fetched row. Instead, it defines *scopes*, which you access with the `scoped(on:)` method. It returns an optional Row, which is nil if the scope is missing. + +```swift +row // <Row a:0 b:1 c:2 d:3> +row.scoped(on: "left") // <Row a:0 b:1> +row.scoped(on: "right") // <Row c:2 d:3> +row.scoped(on: "missing") // nil +``` + +Scopes can be nested: + +```swift +let adapter = ScopeAdapter([ + "left": ScopeAdapter([ + "left": RangeRowAdapter(0..<1), + "right": RangeRowAdapter(1..<2)]), + "right": ScopeAdapter([ + "left": RangeRowAdapter(2..<3), + "right": RangeRowAdapter(3..<4)]) + ]) +let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d", adapter: adapter)! + +let leftRow = row.scoped(on: "left")! +leftRow.scoped(on: "left") // <Row a:0> +leftRow.scoped(on: "right") // <Row b:1> + +let rightRow = row.scoped(on: "right")! +rightRow.scoped(on: "left") // <Row c:2> +rightRow.scoped(on: "right") // <Row d:3> +``` + +Any adapter can be extended with scopes: + +```swift +let adapter = RangeRowAdapter(0..<2) + .addingScopes(["remainder": SuffixRowAdapter(fromIndex: 2)]) +let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d", adapter: adapter)! + +row // <Row a:0 b:1> +row.scoped(on: "remainder") // <Row c:2 d:3> +``` + + +## Raw SQLite Pointers + +**If not all SQLite APIs are exposed in GRDB, you can still use the [SQLite C Interface](https://www.sqlite.org/c3ref/intro.html) and call [SQLite C functions](https://www.sqlite.org/c3ref/funclist.html).** + +Those functions are embedded right into the GRDBCustom and GRCBCipher modules. For the "regular" GRDB framework: you'll need to import `SQLite3`, or `CSQLite`, depending on whether you use the Swift Package Manager or not: + +```swift +#if SWIFT_PACKAGE + import CSQLite // For Swift Package Manager +#else + import SQLite3 // Otherwise +#endif + +let sqliteVersion = String(cString: sqlite3_libversion()) +``` + +Raw pointers to database connections and statements are available through the `Database.sqliteConnection` and `Statement.sqliteStatement` properties: + +```swift +try dbQueue.inDatabase { db in + // The raw pointer to a database connection: + let sqliteConnection = db.sqliteConnection + + // The raw pointer to a statement: + let statement = try db.makeSelectStatement("SELECT ...") + let sqliteStatement = statement.sqliteStatement +} +``` + +> :point_up: **Notes** +> +> - Those pointers are owned by GRDB: don't close connections or finalize statements created by GRDB. +> - GRDB opens SQLite connections in the "[multi-thread mode](https://www.sqlite.org/threadsafe.html)", which (oddly) means that **they are not thread-safe**. Make sure you touch raw databases and statements inside their dedicated dispatch queues. +> - Use the raw SQLite C Interface at your own risk. GRDB won't prevent you from shooting yourself in the foot. + +Before jumping in the low-level wagon, here is the list of all SQLite APIs used by GRDB: + +- `sqlite3_aggregate_context`, `sqlite3_create_function_v2`, `sqlite3_result_blob`, `sqlite3_result_double`, `sqlite3_result_error`, `sqlite3_result_error_code`, `sqlite3_result_int64`, `sqlite3_result_null`, `sqlite3_result_text`, `sqlite3_user_data`, `sqlite3_value_blob`, `sqlite3_value_bytes`, `sqlite3_value_double`, `sqlite3_value_int64`, `sqlite3_value_text`, `sqlite3_value_type`: see [Custom SQL Functions and Aggregates](#custom-sql-functions-and-aggregates) +- `sqlite3_backup_finish`, `sqlite3_backup_init`, `sqlite3_backup_step`: see [Backup](#backup) +- `sqlite3_bind_blob`, `sqlite3_bind_double`, `sqlite3_bind_int64`, `sqlite3_bind_null`, `sqlite3_bind_parameter_count`, `sqlite3_bind_parameter_name`, `sqlite3_bind_text`, `sqlite3_clear_bindings`, `sqlite3_column_blob`, `sqlite3_column_bytes`, `sqlite3_column_count`, `sqlite3_column_double`, `sqlite3_column_int64`, `sqlite3_column_name`, `sqlite3_column_text`, `sqlite3_column_type`, `sqlite3_exec`, `sqlite3_finalize`, `sqlite3_prepare_v2`, `sqlite3_reset`, `sqlite3_step`: see [Executing Updates](#executing-updates), [Fetch Queries](#fetch-queries), [Prepared Statements](#prepared-statements), [Values](#values) +- `sqlite3_busy_handler`, `sqlite3_busy_timeout`: see [Configuration.busyMode](http://groue.github.io/GRDB.swift/docs/2.0/Structs/Configuration.html) +- `sqlite3_changes`, `sqlite3_total_changes`: see [Database.changesCount and Database.totalChangesCount](http://groue.github.io/GRDB.swift/docs/2.0/Classes/Database.html) +- `sqlite3_close`, `sqlite3_close_v2`, `sqlite3_next_stmt`, `sqlite3_open_v2`: see [Database Connections](#database-connections) +- `sqlite3_commit_hook`, `sqlite3_rollback_hook`, `sqlite3_update_hook`: see [TransactionObserver Protocol](#transactionobserver-protocol), [FetchedRecordsController](#fetchedrecordscontroller) +- `sqlite3_config`: see [Error Log](#error-log) +- `sqlite3_create_collation_v2`: see [String Comparison](#string-comparison) +- `sqlite3_db_release_memory`: see [Memory Management](#memory-management) +- `sqlite3_errcode`, `sqlite3_errmsg`, `sqlite3_errstr`, `sqlite3_extended_result_codes`: see [Error Handling](#error-handling) +- `sqlite3_key`, `sqlite3_rekey`: see [Encryption](#encryption) +- `sqlite3_last_insert_rowid`: see [Executing Updates](#executing-updates) +- `sqlite3_preupdate_count`, `sqlite3_preupdate_depth`, `sqlite3_preupdate_hook`, `sqlite3_preupdate_new`, `sqlite3_preupdate_old`: see [Support for SQLite Pre-Update Hooks](#support-for-sqlite-pre-update-hooks) +- `sqlite3_set_authorizer`: **reserved by GRDB** +- `sqlite3_sql`: see [Statement.sql](http://groue.github.io/GRDB.swift/docs/2.0/Classes/Statement.html) +- `sqlite3_trace`: see [Configuration.trace](http://groue.github.io/GRDB.swift/docs/2.0/Structs/Configuration.html) +- `sqlite3_wal_checkpoint_v2`: see [DatabasePool.checkpoint](http://groue.github.io/GRDB.swift/docs/2.0/Classes/DatabasePool.html) + + +Records +======= + +**On top of the [SQLite API](#sqlite-api), GRDB provides protocols and a class** that help manipulating database rows as regular objects named "records": + +```swift +try dbQueue.inDatabase { db in + if let place = try Place.fetchOne(db, key: 1) { + place.isFavorite = true + try place.update(db) + } +} +``` + +Of course, you need to open a [database connection](#database-connections), and [create a database table](#database-schema) first. + +Your custom structs and classes can adopt each protocol individually, and opt in to focused sets of features. Or you can subclass the `Record` class, and get the full toolkit in one go: fetching methods, persistence methods, and changes tracking. See the [list of record methods](#list-of-record-methods) for an overview. + +> :point_up: **Note**: if you are familiar with Core Data's NSManagedObject or Realm's Object, you may experience a cultural shock: GRDB records are not uniqued, and do not auto-update. This is both a purpose, and a consequence of protocol-oriented programming. You should read [How to build an iOS application with SQLite and GRDB.swift](https://medium.com/@gwendal.roue/how-to-build-an-ios-application-with-sqlite-and-grdb-swift-d023a06c29b3) for a general introduction. + +**Overview** + +- [Inserting Records](#inserting-records) +- [Fetching Records](#fetching-records) +- [Updating Records](#updating-records) +- [Deleting Records](#deleting-records) +- [Counting Records](#counting-records) + +**Protocols and the Record class** + +- [Record Protocols Overview](#record-protocols-overview) +- [RowConvertible Protocol](#rowconvertible-protocol) +- [TableMapping Protocol](#tablemapping-protocol) +- [Persistable Protocol](#persistable-protocol) + - [Persistence Methods](#persistence-methods) + - [Customizing the Persistence Methods](#customizing-the-persistence-methods) + - [Conflict Resolution](#conflict-resolution) +- [Record Class](#record-class) + - [Changes Tracking](#changes-tracking) +- [The Implicit RowID Primary Key](#the-implicit-rowid-primary-key) +- **[List of Record Methods](#list-of-record-methods)** + +**Records, Swift Archival & Serialization** + +- [Codable Records](#codable-records) + + +### Inserting Records + +To insert a record in the database, subclass the [Record](#record-class) class or adopt the [Persistable](#persistable-protocol) protocol, and call the `insert` method: + +```swift +class Player : Record { ... } + +let player = Player(name: "Arthur", email: "arthur@example.com") +try player.insert(db) +``` + + +### Fetching Records + +[Record](#record-class) subclasses and types that adopt the [RowConvertible](#rowconvertible-protocol) protocol can be fetched from the database: + +```swift +class Player : Record { ... } +let players = try Player.fetchAll(db, "SELECT ...", arguments: ...) // [Player] +``` + +Add the [TableMapping](#tablemapping-protocol) protocol and you can stop writing SQL: + +```swift +let spain = try Country.fetchOne(db, key: "ES") // Country? +let players = try Player // [Player] + .filter(Column("email") != nil) + .order(Column("name")) + .fetchAll(db) +``` + +See [fetching methods](#fetching-methods), and the [query interface](#the-query-interface). + + +### Updating Records + +[Record](#record-class) subclasses and types that adopt the [Persistable](#persistable-protocol) protocol can be updated in the database: + +```swift +let player = try Player.fetchOne(db, key: 1)! +player.name = "Arthur" +try player.update(db) +``` + +[Record](#record-class) subclasses track changes, so that you can avoid useless updates: + +```swift +let player = try Player.fetchOne(db, key: 1)! +player.name = "Arthur" +if player.hasPersistentChangedValues { + try player.update(db) +} +``` + +For batch updates, execute an [SQL query](#executing-updates): + +```swift +try db.execute("UPDATE players SET synchronized = 1") +``` + + +### Deleting Records + +[Record](#record-class) subclasses and types that adopt the [Persistable](#persistable-protocol) protocol can be deleted from the database: + +```swift +let player = try Player.fetchOne(db, key: 1)! +try player.delete(db) +``` + +Such records can also delete according to primary key or any unique index: + +```swift +try Player.deleteOne(db, key: 1) +try Player.deleteOne(db, key: ["email": "arthur@example.com"]) +try Country.deleteAll(db, keys: ["FR", "US"]) +``` + +For batch deletes, see the [query interface](#the-query-interface): + +```swift +try Player.filter(emailColumn == nil).deleteAll(db) +``` + + +### Counting Records + +[Record](#record-class) subclasses and types that adopt the [TableMapping](#tablemapping-protocol) protocol can be counted: + +```swift +let playerWithEmailCount = try Player.filter(emailColumn != nil).fetchCount(db) // Int +``` + + +You can now jump to: + +- [Record Protocols Overview](#record-protocols-overview) +- [RowConvertible Protocol](#rowconvertible-protocol) +- [TableMapping Protocol](#tablemapping-protocol) +- [Persistable Protocol](#persistable-protocol) +- [Record Class](#record-class) +- [List of Record Methods](#list-of-record-methods) +- [The Query Interface](#the-query-interface) + + +## Record Protocols Overview + +**GRDB ships with three record protocols**. Your own types will adopt one or several of them, according to the abilities you want to extend your types with. + +- [RowConvertible](#rowconvertible-protocol) is able to **read**: it grants the ability to efficiently decode raw database row. + + Imagine you want to load places from the `places` database table. + + One way to do it is to load raw database rows: + + ```swift + func fetchPlaceRows(_ db: Database) throws -> [Row] { + return try Row.fetchAll(db, "SELECT * FROM places") + } + ``` + + The problem is that [raw rows](#row-queries) are not easy to deal with, and you may prefer using a proper `Place` type: + + ```swift + // Dedicated model + struct Place { ... } + func fetchPlaces(_ db: Database) throws -> [Place] { + let rows = try Row.fetchAll(db, "SELECT * FROM places") + return rows.map { row in + Place( + id: row["id"], + title: row["title"], + coordinate: CLLocationCoordinate2D( + latitude: row["latitude"], + longitude: row["longitude"])) + ) + } + } + ``` + + This code is verbose, and so you define an `init(row:)` initializer: + + ```swift + // Row initializer + struct Place { + init(row: Row) { + id = row["id"] + ... + } + } + func fetchPlaces(_ db: Database) throws -> [Place] { + let rows = try Row.fetchAll(db, "SELECT * FROM places") + return rows.map { Place(row: $0) } + } + ``` + + Now you notice that this code may use a lot of memory when you have many rows: a full array of database rows is created in order to build an array of places. Furthermore, rows that have been copied from the database have lost the ability to directly load values from SQLite: that's inefficient. You thus use a [database cursor](#cursors), which is both lazy and efficient: + + ```swift + // Cursor for efficiency + func fetchPlaces(_ db: Database) throws -> [Place] { + let rowCursor = try Row.fetchCursor(db, "SELECT * FROM places") + let placeCursor = rowCursor.map { Place(row: $0) } + return try Array(placeCursor) + } + ``` + + That's better. And that's what RowConvertible does, with a little performance bonus, and in a single line: + + ```swift + struct Place : RowConvertible { + init(row: Row) { ... } + } + func fetchPlaces(_ db: Database) throws -> [Place] { + return try Place.fetchAll(db, "SELECT * FROM places") + } + ``` + + RowConvertible is not able to build SQL requests, though. For that, you also need TableMapping: + +- [TableMapping](#tablemapping-protocol) is able to **build requests without SQL**: + + ```swift + struct Place : TableMapping { ... } + // SELECT * FROM places ORDER BY title + let request = Place.order(Column("title")) + ``` + + When a type adopts both TableMapping and RowConvertible, it can load from those requests: + + ```swift + struct Place : TableMapping, RowConvertible { ... } + try dbQueue.inDatabase { db in + let places = try Place.order(Column("title")).fetchAll(db) + let paris = try Place.fetchOne(key: 1) + } + ``` + +- [Persistable](#persistable-protocol) is able to **write**: it can create, update, and delete rows in the database: + + ```swift + struct Place : Persistable { ... } + try dbQueue.inDatabase { db in + try Place.delete(db, key: 1) + try Place(...).insert(db) + } + ``` + + +## RowConvertible Protocol + +**The RowConvertible protocol grants fetching methods to any type** that can be built from a database row: + +```swift +protocol RowConvertible { + /// Row initializer + init(row: Row) +} +``` + +**To use RowConvertible**, subclass the [Record](#record-class) class, or adopt it explicitely. For example: + +```swift +struct Place { + var id: Int64? + var title: String + var coordinate: CLLocationCoordinate2D +} + +extension Place : RowConvertible { + init(row: Row) { + id = row["id"] + title = row["title"] + coordinate = CLLocationCoordinate2D( + latitude: row["latitude"], + longitude: row["longitude"]) + } +} +``` + +Rows also accept keys of type `Column`: + +```swift +extension Place : RowConvertible { + enum Columns { + static let id = Column("id") + static let title = Column("title") + static let latitude = Column("latitude") + static let longitude = Column("longitude") + } + + init(row: Row) { + id = row[Columns.id] + title = row[Columns.title] + coordinate = CLLocationCoordinate2D( + latitude: row[Columns.latitude], + longitude: row[Columns.longitude]) + } +} +``` + +See [column values](#column-values) for more information about the `row[]` subscript. + +> :point_up: **Note**: for performance reasons, the same row argument to `init(row:)` is reused during the iteration of a fetch query. If you want to keep the row for later use, make sure to store a copy: `self.row = row.copy()`. + +**The `init(row:)` initializer can be automatically generated** when your type adopts the standard `Decodable` protocol. See [Codable Records](#codable-records) for more information. + +RowConvertible allows adopting types to be fetched from SQL queries: + +```swift +try Place.fetchCursor(db, "SELECT ...", arguments:...) // A Cursor of Place +try Place.fetchAll(db, "SELECT ...", arguments:...) // [Place] +try Place.fetchOne(db, "SELECT ...", arguments:...) // Place? +``` + +See [fetching methods](#fetching-methods) for information about the `fetchCursor`, `fetchAll` and `fetchOne` methods. See [StatementArguments](http://groue.github.io/GRDB.swift/docs/2.0/Structs/StatementArguments.html) for more information about the query arguments. + + +## TableMapping Protocol + +**Adopt the TableMapping protocol** on top of [RowConvertible](#rowconvertible-protocol), and you are granted with the full [query interface](#the-query-interface). + +```swift +protocol TableMapping { + static var databaseTableName: String { get } + static var databaseSelection: [SQLSelectable] { get } +} +``` + +The `databaseTableName` type property is the name of a database table. `databaseSelection` is optional, and documented in the [Columns Selected by a Request](#columns-selected-by-a-request) chapter. + +**To use TableMapping**, subclass the [Record](#record-class) class, or adopt it explicitely. For example: + +```swift +extension Place : TableMapping { + static let databaseTableName = "places" +} +``` + +Adopting types can be fetched without SQL, using the [query interface](#the-query-interface): + +```swift +// SELECT * FROM places WHERE name = 'Paris' +let paris = try Place.filter(nameColumn == "Paris").fetchOne(db) +``` + +TableMapping can also fetch records by primary key: + +```swift +try Player.fetchOne(db, key: 1) // Player? +try Player.fetchAll(db, keys: [1, 2, 3]) // [Player] + +try Country.fetchOne(db, key: "FR") // Country? +try Country.fetchAll(db, keys: ["FR", "US"]) // [Country] +``` + +When the table has no explicit primary key, GRDB uses the [hidden "rowid" column](#the-implicit-rowid-primary-key): + +```swift +// SELECT * FROM documents WHERE rowid = 1 +try Document.fetchOne(db, key: 1) // Document? +``` + +For multiple-column primary keys and unique keys defined by unique indexes, provide a dictionary: + +```swift +// SELECT * FROM citizenships WHERE playerID = 1 AND countryISOCode = 'FR' +try Citizenship.fetchOne(db, key: ["playerID": 1, "countryISOCode": "FR"]) // Citizenship? +``` + + +## Persistable Protocol + +**GRDB provides two protocols that let adopting types create, update, and delete rows in the database:** + +```swift +protocol MutablePersistable : TableMapping { + /// The name of the database table (from TableMapping) + static var databaseTableName: String { get } + + /// Defines the values persisted in the database + func encode(to container: inout PersistenceContainer) + + /// Optional method that lets your adopting type store its rowID upon + /// successful insertion. Don't call it directly: it is called for you. + mutating func didInsert(with rowID: Int64, for column: String?) +} +``` + +```swift +protocol Persistable : MutablePersistable { + /// Non-mutating version of the optional didInsert(with:for:) + func didInsert(with rowID: Int64, for column: String?) +} +``` + +Yes, two protocols instead of one. Both grant exactly the same advantages. Here is how you pick one or the other: + +- *If your type is a struct that mutates on insertion*, choose `MutablePersistable`. + + For example, your table has an INTEGER PRIMARY KEY and you want to store the inserted id on successful insertion. Or your table has a UUID primary key, and you want to automatically generate one on insertion. + +- Otherwise, stick with `Persistable`. Particularly if your type is a class. + +The `encode(to:)` method defines which [values](#values) (Bool, Int, String, Date, Swift enums, etc.) are assigned to database columns. + +**`encode(to:)` can be automatically generated** when your type adopts the standard `Encodable` protocol. See [Codable Records](#codable-records) for more information. + +The optional `didInsert` method lets the adopting type store its rowID after successful insertion. If your table has an INTEGER PRIMARY KEY column, you are likely to define this method. Otherwise, you can safely ignore it. It is called from a protected dispatch queue, and serialized with all database updates. + +**To use those protocols**, subclass the [Record](#record-class) class, or adopt one of them explicitely. For example: + +```swift +extension Place : MutablePersistable { + + /// The values persisted in the database + func encode(to container: inout PersistenceContainer) { + container["id"] = id + container["title"] = title + container["latitude"] = coordinate.latitude + container["longitude"] = coordinate.longitude + } + + // Update id upon successful insertion: + mutating func didInsert(with rowID: Int64, for column: String?) { + id = rowID + } +} + +var paris = Place( + id: nil, + title: "Paris", + coordinate: CLLocationCoordinate2D(latitude: 48.8534100, longitude: 2.3488000)) + +try paris.insert(db) +paris.id // some value +``` + +Persistence containers also accept keys of type `Column`: + +```swift +extension Place : MutablePersistable { + enum Columns { + static let id = Column("id") + static let title = Column("title") + static let latitude = Column("latitude") + static let longitude = Column("longitude") + } + + func encode(to container: inout PersistenceContainer) { + container[Columns.id] = id + container[Columns.title] = title + container[Columns.latitude] = coordinate.latitude + container[Columns.longitude] = coordinate.longitude + } +} +``` + + +### Persistence Methods + +[Record](#record-class) subclasses and types that adopt [Persistable](#persistable-protocol) are given default implementations for methods that insert, update, and delete: + +```swift +// Instance methods +try place.save(db) // Inserts or updates +try place.insert(db) // INSERT +try place.update(db) // UPDATE +try place.update(db, columns: ...) // UPDATE +try place.updateChanges(db) // Available for the Record class only +try place.delete(db) // DELETE +place.exists(db) + +// Type methods +Place.deleteAll(db) // DELETE +Place.deleteAll(db, keys:...) // DELETE +Place.deleteOne(db, key:...) // DELETE +``` + +- `insert`, `update`, `save` and `delete` can throw a [DatabaseError](#error-handling). + +- `update` and `updateChanges` can also throw a [PersistenceError](#persistenceerror), should the update fail because there is no matching row in the database. + + When saving an object that may or may not already exist in the database, prefer the `save` method: + +- `save` makes sure your values are stored in the database. + + It performs an UPDATE if the record has a non-null primary key, and then, if no row was modified, an INSERT. It directly perfoms an INSERT if the record has no primary key, or a null primary key. + + Despite the fact that it may execute two SQL statements, `save` behaves as an atomic operation: GRDB won't allow any concurrent thread to sneak in (see [concurrency](#concurrency)). + +- `delete` returns whether a database row was deleted or not. + +**All primary keys are supported**, including composite primary keys that span several columns, and the [implicit rowid primary key](#the-implicit-rowid-primary-key). + + +### Customizing the Persistence Methods + +Your custom type may want to perform extra work when the persistence methods are invoked. + +For example, it may want to have its UUID automatically set before inserting. Or it may want to validate its values before saving. + +When you subclass [Record](#record-class), you simply have to override the customized method, and call `super`: + +```swift +class Player : Record { + var uuid: UUID? + + override func insert(_ db: Database) throws { + if uuid == nil { + uuid = UUID() + } + try super.insert(db) + } +} +``` + +If you use the raw [Persistable](#persistable-protocol) protocol, use one of the *special methods* `performInsert`, `performUpdate`, `performSave`, `performDelete`, or `performExists`: + +```swift +struct Link : Persistable { + var url: URL + + func insert(_ db: Database) throws { + try validate() + try performInsert(db) + } + + func update(_ db: Database, columns: Set<String>) throws { + try validate() + try performUpdate(db, columns: columns) + } + + func validate() throws { + if url.host == nil { + throw ValidationError("url must be absolute.") + } + } +} +``` + +> :point_up: **Note**: the special methods `performInsert`, `performUpdate`, etc. are reserved for your custom implementations. Do not use them elsewhere. Do not provide another implementation for those methods. +> +> :point_up: **Note**: it is recommended that you do not implement your own version of the `save` method. Its default implementation forwards the job to `update` or `insert`: these are the methods that may need customization, not `save`. + + +### Conflict Resolution + +**Insertions and updates can create conflicts**: for example, a query may attempt to insert a duplicate row that violates a unique index. + +Those conflicts normally end with an error. Yet SQLite let you alter the default behavior, and handle conflicts with specific policies. For example, the `INSERT OR REPLACE` statement handles conflicts with the "replace" policy which replaces the conflicting row instead of throwing an error. + +The [five different policies](https://www.sqlite.org/lang_conflict.html) are: abort (the default), replace, rollback, fail, and ignore. + +**SQLite let you specify conflict policies at two different places:** + +- At the table level + + ```swift + // CREATE TABLE players ( + // id INTEGER PRIMARY KEY, + // email TEXT UNIQUE ON CONFLICT REPLACE + // ) + try db.create(table: "players") { t in + t.column("id", .integer).primaryKey() + t.column("email", .text).unique(onConflict: .replace) // <-- + } + + // Despite the unique index on email, both inserts succeed. + // The second insert replaces the first row: + try db.execute("INSERT INTO players (email) VALUES (?)", arguments: ["arthur@example.com"]) + try db.execute("INSERT INTO players (email) VALUES (?)", arguments: ["arthur@example.com"]) + ``` + +- At the query level: + + ```swift + // CREATE TABLE players ( + // id INTEGER PRIMARY KEY, + // email TEXT UNIQUE + // ) + try db.create(table: "players") { t in + t.column("id", .integer).primaryKey() + t.column("email", .text) + } + + // Again, despite the unique index on email, both inserts succeed. + try db.execute("INSERT OR REPLACE INTO players (email) VALUES (?)", arguments: ["arthur@example.com"]) + try db.execute("INSERT OR REPLACE INTO players (email) VALUES (?)", arguments: ["arthur@example.com"]) + ``` + +When you want to handle conflicts at the query level, specify a custom `persistenceConflictPolicy` in your type that adopts the MutablePersistable or Persistable protocol. It will alter the INSERT and UPDATE queries run by the `insert`, `update` and `save` [persistence methods](#persistence-methods): + +```swift +protocol MutablePersistable { + /// The policy that handles SQLite conflicts when records are inserted + /// or updated. + /// + /// This property is optional: its default value uses the ABORT policy + /// for both insertions and updates, and has GRDB generate regular + /// INSERT and UPDATE queries. + static var persistenceConflictPolicy: PersistenceConflictPolicy { get } +} + +struct Player : MutablePersistable { + static let persistenceConflictPolicy = PersistenceConflictPolicy( + insert: .replace, + update: .replace) +} + +// INSERT OR REPLACE INTO players (...) VALUES (...) +try player.insert(db) +``` + +> :point_up: **Note**: the `ignore` policy does not play well at all with the `didInsert` method which notifies the rowID of inserted records. Choose your poison: +> +> - if you specify the `ignore` policy at the table level, don't implement the `didInsert` method: it will be called with some random id in case of failed insert. +> - if you specify the `ignore` policy at the query level, the `didInsert` method is never called. +> +> :warning: **Warning**: [`ON CONFLICT REPLACE`](https://www.sqlite.org/lang_conflict.html) may delete rows so that inserts and updates can succeed. Those deletions are not reported to [transaction observers](#transactionobserver-protocol) (this might change in a future release of SQLite). + + +## Record Class + +**Record** is a class that is designed to be subclassed, and provides the full toolkit in one go: fetching and persistence methods, as well as changes tracking (see the [list of record methods](#list-of-record-methods) for an overview). + +Record subclasses inherit their features from the [RowConvertible, TableMapping, and Persistable](#record-protocols-overview) protocols. Check their documentation for more information. + +For example, here is a fully functional Record subclass: + +```swift +class Place : Record { + var id: Int64? + var title: String + var coordinate: CLLocationCoordinate2D + + /// The table name + override class var databaseTableName: String { + return "places" + } + + /// Initialize from a database row + required init(row: Row) { + id = row["id"] + title = row["title"] + coordinate = CLLocationCoordinate2D( + latitude: row["latitude"], + longitude: row["longitude"]) + super.init(row: row) + } + + /// The values persisted in the database + override func encode(to container: inout PersistenceContainer) { + container["id"] = id + container["title"] = title + container["latitude"] = coordinate.latitude + container["longitude"] = coordinate.longitude + } + + /// Update record ID after a successful insertion + override func didInsert(with rowID: Int64, for column: String?) { + id = rowID + } +} +``` + + +### Changes Tracking + +**The [Record](#record-class) class provides changes tracking.** + +The `update()` [method](#persistence-methods) always executes an UPDATE statement. When the record has not been edited, this costly database access is generally useless. + +Avoid it with the `hasPersistentChangedValues` property, which returns whether the record has changes that have not been saved: + +```swift +// Insert or update the player if it has unsaved changes +if player.hasPersistentChangedValues { + try player.save(db) +} +``` + +You can also use the `updateChanges` method, which performs an update of the changed columns (and does nothing if record has no change): + +```swift +// Update the unsaved player changes +try player.updateChanges(db) +``` + +The `hasPersistentChangedValues` flag is false after a record has been fetched or saved into the database. Subsequent modifications may set it, or not: `hasPersistentChangedValues` is based on value comparison. **Setting a property to the same value does not set the changed flag**: + +```swift +let player = Player(name: "Barbara", score: 750) +player.hasPersistentChangedValues // true + +try player.insert(db) +player.hasPersistentChangedValues // false + +player.name = "Barbara" +player.hasPersistentChangedValues // false + +player.score = 1000 +player.hasPersistentChangedValues // true +player.persistentChangedValues // ["score": 750] +``` + +For an efficient algorithm which synchronizes the content of a database table with a JSON payload, check [JSONSynchronization.playground](Playgrounds/JSONSynchronization.playground/Contents.swift). + + +## The Implicit RowID Primary Key + +**All SQLite tables have a primary key.** Even when the primary key is not explicit: + +```swift +// No explicit primary key +try db.create(table: "events") { t in + t.column("message", .text) + t.column("date", .datetime) +} + +// No way to define an explicit primary key +try db.create(virtualTable: "books", using: FTS4()) { t in + t.column("title") + t.column("author") + t.column("body") +} +``` + +The implicit primary key is stored in the hidden column `rowid`. Hidden means that `SELECT *` does not select it, and yet it can be selected and queried: `SELECT *, rowid ... WHERE rowid = 1`. + +Some GRDB methods will automatically use this hidden column when a table has no explicit primary key: + +```swift +// SELECT * FROM events WHERE rowid = 1 +let event = try Event.fetchOne(db, key: 1) + +// DELETE FROM books WHERE rowid = 1 +try Book.deleteOne(db, key: 1) +``` + + +### Exposing the RowID Column + +**By default, a record type that wraps a table without any explicit primary key doesn't know about the hidden rowid column.** + +Without primary key, records don't have any identity, and the [persistence method](#persistence-methods) can behave in undesired fashion: `update()` throws errors, `save()` always performs insertions and may break constraints, `exists()` is always false. + +When SQLite won't let you provide an explicit primary key (as in [full-text](#full-text-search) tables, for example), you may want to make your record type fully aware of the hidden rowid column: + +1. Have the `databaseSelection` static property (from the [TableMapping](#tablemapping-protocol) protocol) return the hidden rowid column: + + ```swift + struct Event : TableMapping { + static let databaseSelection: [SQLSelectable] = [AllColumns(), Column.rowID] + } + + // When you subclass Record, you need an override: + class Book : Record { + override class var databaseSelection: [SQLSelectable] { + return [AllColums(), Column.rowID] + } + } + ``` + + GRDB will then select the `rowid` column by default: + + ```swift + // SELECT *, rowid FROM events + let events = try Event.fetchAll(db) + ``` + +2. Have `init(row:)` from the [RowConvertible](#rowconvertible-protocol) protocol consume the "rowid" column: + + ```swift + struct Event : RowConvertible { + var id: Int64? + + init(row: Row) { + id = row["rowid"] + } + } + ``` + + If you prefer using the Column type from the [query interface](#the-query-interface), use the `Column.rowID` constant: + + ```swift + init(row: Row) { + id = row[.rowID] + } + ``` + + Your fetched records will then know their ids: + + ```swift + let event = try Event.fetchOne(db)! + event.id // some value + ``` + +3. Encode the rowid in `encode(to:)`, and keep it in the `didInsert(with:for:)` method (both from the [Persistable and MutablePersistable](#persistable-protocol) protocols): + + ```swift + struct Event : MutablePersistable { + var id: Int64? + + func encode(to container: inout PersistenceContainer) { + container[.rowID] = id + container["message"] = message + container["date"] = date + } + + mutating func didInsert(with rowID: Int64, for column: String?) { + id = rowID + } + } + ``` + + You will then be able to track your record ids, update them, or check for their existence: + + ```swift + let event = Event(message: "foo", date: Date()) + + // Insertion sets the record id: + try event.insert(db) + event.id // some value + + // Record can be updated: + event.message = "bar" + try event.update(db) + + // Record knows if it exists: + event.exists(db) // true + ``` + + +## List of Record Methods + +This is the list of record methods, along with their required protocols. The [Record Class](#record-class) adopts all these protocols. + +| Method | Protocols | Notes | +| ------ | --------- | :---: | +| **Inserting and Updating Records** | | | +| `try record.insert(db)` | [Persistable](#persistable-protocol) | | +| `try record.save(db)` | [Persistable](#persistable-protocol) | | +| `try record.update(db)` | [Persistable](#persistable-protocol) | | +| `try record.update(db, columns: ...)` | [Persistable](#persistable-protocol) | | +| `record.databaseDictionary` | [Persistable](#persistable-protocol) | | +| **Checking Record Existence** | | | +| `record.exists(db)` | [Persistable](#persistable-protocol) | | +| **Deleting Records** | | | +| `try record.delete(db)` | [Persistable](#persistable-protocol) | | +| `try Type.deleteOne(db, key: ...)` | [Persistable](#persistable-protocol) | <a href="#list-of-record-methods-1">¹</a> | +| `try Type.deleteAll(db)` | [Persistable](#persistable-protocol) | | +| `try Type.deleteAll(db, keys: ...)` | [Persistable](#persistable-protocol) | <a href="#list-of-record-methods-1">¹</a> | +| `try Type.filter(...).deleteAll(db)` | [Persistable](#persistable-protocol) | <a href="#list-of-record-methods-2">²</a> | +| **Counting Records** | | | +| `Type.fetchCount(db)` | [TableMapping](#tablemapping-protocol) | | +| `Type.filter(...).fetchCount(db)` | [TableMapping](#tablemapping-protocol) | <a href="#list-of-record-methods-2">²</a> | +| **Fetching Record [Cursors](#cursors)** | | | +| `Type.fetchCursor(db)` | [RowConvertible](#rowconvertible-protocol) & [TableMapping](#tablemapping-protocol) | | +| `Type.fetchCursor(db, keys: ...)` | [RowConvertible](#rowconvertible-protocol) & [TableMapping](#tablemapping-protocol) | <a href="#list-of-record-methods-1">¹</a> | +| `Type.fetchCursor(db, sql)` | [RowConvertible](#rowconvertible-protocol) | <a href="#list-of-record-methods-3">³</a> | +| `Type.fetchCursor(statement)` | [RowConvertible](#rowconvertible-protocol) | <a href="#list-of-record-methods-4">⁴</a> | +| `Type.filter(...).fetchCursor(db)` | [RowConvertible](#rowconvertible-protocol) & [TableMapping](#tablemapping-protocol) | <a href="#list-of-record-methods-2">²</a> | +| **Fetching Record Arrays** | | | +| `Type.fetchAll(db)` | [RowConvertible](#rowconvertible-protocol) & [TableMapping](#tablemapping-protocol) | | +| `Type.fetchAll(db, keys: ...)` | [RowConvertible](#rowconvertible-protocol) & [TableMapping](#tablemapping-protocol) | <a href="#list-of-record-methods-1">¹</a> | +| `Type.fetchAll(db, sql)` | [RowConvertible](#rowconvertible-protocol) | <a href="#list-of-record-methods-3">³</a> | +| `Type.fetchAll(statement)` | [RowConvertible](#rowconvertible-protocol) | <a href="#list-of-record-methods-4">⁴</a> | +| `Type.filter(...).fetchAll(db)` | [RowConvertible](#rowconvertible-protocol) & [TableMapping](#tablemapping-protocol) | <a href="#list-of-record-methods-2">²</a> | +| **Fetching Individual Records** | | | +| `Type.fetchOne(db)` | [RowConvertible](#rowconvertible-protocol) & [TableMapping](#tablemapping-protocol) | | +| `Type.fetchOne(db, key: ...)` | [RowConvertible](#rowconvertible-protocol) & [TableMapping](#tablemapping-protocol) | <a href="#list-of-record-methods-1">¹</a> | +| `Type.fetchOne(db, sql)` | [RowConvertible](#rowconvertible-protocol) | <a href="#list-of-record-methods-3">³</a> | +| `Type.fetchOne(statement)` | [RowConvertible](#rowconvertible-protocol) | <a href="#list-of-record-methods-4">⁴</a> | +| `Type.filter(...).fetchOne(db)` | [RowConvertible](#rowconvertible-protocol) & [TableMapping](#tablemapping-protocol) | <a href="#list-of-record-methods-2">²</a> | +| **Changes Tracking** | | | +| `record.hasPersistentChangedValues` | [Record](#record-class) | | +| `record.persistentChangedValues` | [Record](#record-class) | | +| `record.updateChanges(db)` | [Record](#record-class) | | + +<a name="list-of-record-methods-1">¹</a> All unique keys are supported: primary keys (single-column, composite, [implicit RowID](#the-implicit-rowid-primary-key)) and unique indexes: + +```swift +try Player.fetchOne(db, key: 1) // Player? +try Player.fetchOne(db, key: ["email": "arthur@example.com"]) // Player? +try Country.fetchAll(db, keys: ["FR", "US"]) // [Country] +``` + +<a name="list-of-record-methods-2">²</a> See [Fetch Requests](#requests): + +```swift +let request = Player.filter(emailColumn != nil).order(nameColumn) +let players = try request.fetchAll(db) // [Player] +let count = try request.fetchCount(db) // Int +``` + +<a name="list-of-record-methods-3">³</a> See [SQL queries](#fetch-queries): + +```swift +let player = try Player.fetchOne("SELECT * FROM players WHERE id = ?", arguments: [1]) // Player? +``` + +<a name="list-of-record-methods-4">⁴</a> See [Prepared Statements](#prepared-statements): + +```swift +let statement = try db.makeSelectStatement("SELECT * FROM players WHERE id = ?") +let player = try Player.fetchOne(statement, arguments: [1]) // Player? +``` + + +## Codable Records + +[Swift Archival & Serialization](https://github.com/apple/swift-evolution/blob/master/proposals/0166-swift-archival-serialization.md) was introduced with Swift 4. + +GRDB provides default implementations for [`RowConvertible.init(row:)`](#rowconvertible-protocol) and [`Persistable.encode(to:)`](#persistable-protocol) for record types that also adopt an archival protocol (`Codable`, `Encodable` or `Decodable`). When all their properties are themselves codable, Swift generates the archiving methods, and you don't need to write them down: + +```swift +// This is just enough... +struct Player: RowConvertible, Persistable, Codable { + static let databaseTableName = "players" + + let name: String + let score: Int +} + +// ... so that you can save and fetch players: +try dbQueue.inDatabase { db in + try Player(name: "Arthur", score: 100).insert(db) + let players = try Player.fetchAll(db) +} +``` + +> :point_up: **Note**: Some types have a different way to encode and decode themselves in a standard archive vs. the database. For example, [Date](#date-and-datecomponents) saves itself as a numerical timestamp (archive) or a string (database). When such an ambiguity happens, GRDB always favors customized database encoding and decoding. + + +The Query Interface +=================== + +**The query interface lets you write pure Swift instead of SQL:** + +```swift +try dbQueue.inDatabase { db in + // Update database schema + try db.create(table: "wines") { t in ... } + + // Fetch records + let wines = try Wine.filter(origin == "Burgundy").order(price).fetchAll(db) + + // Count + let count = try Wine.filter(color == Color.red).fetchCount(db) + + // Delete + try Wine.filter(corked == true).deleteAll(db) +} +``` + +You need to open a [database connection](#database-connections) before you can query the database. + +Please bear in mind that the query interface can not generate all possible SQL queries. You may also *prefer* writing SQL, and this is just OK. From little snippets to full queries, your SQL skills are welcome: + +```swift +try dbQueue.inDatabase { db in + // Update database schema (with SQL) + try db.execute("CREATE TABLE wines (...)") + + // Fetch records (with SQL) + let wines = try Wine.fetchAll(db, + "SELECT * FROM wines WHERE origin = ? ORDER BY price", + arguments: ["Burgundy"]) + + // Count (with an SQL snippet) + let count = try Wine + .filter(sql: "color = ?", arguments: [Color.red]) + .fetchCount(db) + + // Delete (with SQL) + try db.execute("DELETE FROM wines WHERE corked") +} +``` + +So don't miss the [SQL API](#sqlite-api). + +- [Database Schema](#database-schema) +- [Requests](#requests) + - [Columns Selected by a Request](#columns-selected-by-a-request) +- [Expressions](#expressions) + - [SQL Operators](#sql-operators) + - [SQL Functions](#sql-functions) +- [Fetching from Requests](#fetching-from-requests) +- [Fetching by Key](#fetching-by-key) +- [Fetching Aggregated Values](#fetching-aggregated-values) +- [Delete Requests](#delete-requests) +- [Custom Requests](#custom-requests) +- [GRDB Extension Guide](Documentation/ExtendingGRDB.md) + + +## Database Schema + +Once granted with a [database connection](#database-connections), you can setup your database schema without writing SQL: + +- [Create Tables](#create-tables) +- [Modify Tables](#modify-tables) +- [Drop Tables](#drop-tables) +- [Create Indexes](#create-indexes) + + +### Create Tables + +```swift +// CREATE TABLE places ( +// id INTEGER PRIMARY KEY, +// title TEXT, +// favorite BOOLEAN NOT NULL DEFAULT 0, +// latitude DOUBLE NOT NULL, +// longitude DOUBLE NOT NULL +// ) +try db.create(table: "places") { t in + t.column("id", .integer).primaryKey() + t.column("title", .text) + t.column("favorite", .boolean).notNull().defaults(to: false) + t.column("longitude", .double).notNull() + t.column("latitude", .double).notNull() +} +``` + +The `create(table:)` method covers nearly all SQLite table creation features. For virtual tables, see [Full-Text Search](#full-text-search), or use raw SQL. + +SQLite has many reference documents about table creation: + +- [CREATE TABLE](https://www.sqlite.org/lang_createtable.html) +- [Datatypes In SQLite Version 3](https://www.sqlite.org/datatype3.html) +- [SQLite Foreign Key Support](https://www.sqlite.org/foreignkeys.html) +- [ON CONFLICT](https://www.sqlite.org/lang_conflict.html) +- [The WITHOUT ROWID Optimization](https://www.sqlite.org/withoutrowid.html) + +**Configure table creation**: + +```swift +// CREATE TABLE example ( ... ) +try db.create(table: "example") { t in ... } + +// CREATE TEMPORARY TABLE example IF NOT EXISTS ( +try db.create(table: "example", temporary: true, ifNotExists: true) { t in +``` + +**Add regular columns** with their name and eventual type (text, integer, double, numeric, boolean, blob, date and datetime) - see [SQLite data types](https://www.sqlite.org/datatype3.html): + +```swift +// CREATE TABLE example ( +// a, +// name TEXT, +// creationDate DATETIME, +try db.create(table: "example") { t in ... } + t.column("a") + t.column("name", .text) + t.column("creationDate", .datetime) +``` + +Define **not null** columns, and set **default** values: + +```swift + // email TEXT NOT NULL, + t.column("email", .text).notNull() + + // name TEXT NOT NULL DEFAULT 'Anonymous', + t.column("name", .text).notNull().defaults(to: "Anonymous") +``` + +Use an individual column as **primary**, **unique**, or **foreign key**. When defining a foreign key, the referenced column is the primary key of the referenced table (unless you specify otherwise): + +```swift + // id INTEGER PRIMARY KEY, + t.column("id", .integer).primaryKey() + + // email TEXT UNIQUE, + t.column("email", .text).unique() + + // countryCode TEXT REFERENCES countries(code) ON DELETE CASCADE, + t.column("countryCode", .text).references("countries", onDelete: .cascade) +``` + +**Create an index** on the column: + +```swift + t.column("score", .integer).indexed() +``` + +For extra index options, see [Create Indexes](#create-indexes) below. + +**Perform integrity checks** on individual columns, and SQLite will only let conforming rows in. In the example below, the `$0` closure variable is a column which lets you build any SQL [expression](#expressions). + +```swift + // name TEXT CHECK (LENGTH(name) > 0) + // score INTEGER CHECK (score > 0) + t.column("name", .text).check { length($0) > 0 } + t.column("score", .integer).check(sql: "score > 0") +``` + +Other **table constraints** can involve several columns: + +```swift + // PRIMARY KEY (a, b), + t.primaryKey(["a", "b"]) + + // UNIQUE (a, b) ON CONFLICT REPLACE, + t.uniqueKey(["a", "b"], onConfict: .replace) + + // FOREIGN KEY (a, b) REFERENCES parents(c, d), + t.foreignKey(["a", "b"], references: "parent") + + // CHECK (a + b < 10), + t.check(Column("a") + Column("b") < 10) + + // CHECK (a + b < 10) + t.check(sql: "a + b < 10") +} +``` + +### Modify Tables + +SQLite lets you rename tables, and add columns to existing tables: + +```swift +// ALTER TABLE referers RENAME TO referrers +try db.rename(table: "referers", to: "referrers") + +// ALTER TABLE players ADD COLUMN url TEXT +try db.alter(table: "players") { t in + t.add(column: "url", .text) +} +``` + +> :point_up: **Note**: SQLite restricts the possible table alterations, and may require you to recreate dependent triggers or views. See the documentation of the [ALTER TABLE](https://www.sqlite.org/lang_altertable.html) for details. See [Advanced Database Schema Changes](#advanced-database-schema-changes) for a way to lift restrictions. + + +### Drop Tables + +Drop tables with the `drop(table:)` method: + +```swift +try db.drop(table: "obsolete") +``` + +### Create Indexes + +Create indexes with the `create(index:)` method: + +```swift +// CREATE UNIQUE INDEX byEmail ON users(email) +try db.create(index: "byEmail", on: "users", columns: ["email"], unique: true) +``` + +Relevant SQLite documentation: + +- [CREATE INDEX](https://www.sqlite.org/lang_createindex.html) +- [Indexes On Expressions](https://www.sqlite.org/expridx.html) +- [Partial Indexes](https://www.sqlite.org/partialindex.html) + + +## Requests + +**The query interface requests** let you fetch values from the database: + +```swift +let request = Player.filter(emailColumn != nil).order(nameColumn) +let players = try request.fetchAll(db) // [Player] +let count = try request.fetchCount(db) // Int +``` + +All requests start from **a type** that adopts the `TableMapping` protocol, such as a `Record` subclass (see [Records](#records)): + +```swift +class Player : Record { ... } +``` + +Declare the table **columns** that you want to use for filtering, or sorting: + +```swift +let idColumn = Column("id") +let nameColumn = Column("name") +``` + +You can now build requests with the following methods: `all`, `none`, `select`, `distinct`, `filter`, `matching`, `group`, `having`, `order`, `reversed`, `limit`. All those methods return another request, which you can further refine by applying another method: `Player.select(...).filter(...).order(...)`. + +- `all()`, `none()`: the requests for all rows, or no row. + + ```swift + // SELECT * FROM players + Player.all() + ``` + + The hidden `rowid` column can be selected as well [when you need it](#the-implicit-rowid-primary-key). + +- `select(expression, ...)` defines the selected columns. + + ```swift + // SELECT id, name FROM players + Player.select(idColumn, nameColumn) + + // SELECT MAX(score) AS maxScore FROM players + Player.select(max(scoreColumn).aliased("maxScore")) + ``` + +- `distinct()` performs uniquing. + + ```swift + // SELECT DISTINCT name FROM players + Player.select(nameColumn).distinct() + ``` + +- `filter(expression)` applies conditions. + + ```swift + // SELECT * FROM players WHERE id IN (1, 2, 3) + Player.filter([1,2,3].contains(idColumn)) + + // SELECT * FROM players WHERE (name IS NOT NULL) AND (height > 1.75) + Player.filter(nameColumn != nil && heightColumn > 1.75) + ``` + +- `matching(pattern)` performs [full-text search](#full-text-search). + + ```swift + // SELECT * FROM documents WHERE documents MATCH 'sqlite database' + let pattern = FTS3Pattern(matchingAllTokensIn: "SQLite database") + Document.matching(pattern) + ``` + + When the pattern is nil, no row will match. + +- `group(expression, ...)` groups rows. + + ```swift + // SELECT name, MAX(score) FROM players GROUP BY name + Player + .select(nameColumn, max(scoreColumn)) + .group(nameColumn) + ``` + +- `having(expression)` applies conditions on grouped rows. + + ```swift + // SELECT team, MAX(score) FROM players GROUP BY team HAVING MIN(score) >= 1000 + Player + .select(teamColumn, max(scoreColumn)) + .group(teamColumn) + .having(min(scoreColumn) >= 1000) + ``` + +- `order(ordering, ...)` sorts. + + ```swift + // SELECT * FROM players ORDER BY name + Player.order(nameColumn) + + // SELECT * FROM players ORDER BY score DESC, name + Player.order(scoreColumn.desc, nameColumn) + ``` + + Each `order` call clears any previous ordering: + + ```swift + // SELECT * FROM players ORDER BY name + Player.order(scoreColumn).order(nameColumn) + ``` + +- `reversed()` reverses the eventual orderings. + + ```swift + // SELECT * FROM players ORDER BY score ASC, name DESC + Player.order(scoreColumn.desc, nameColumn).reversed() + ``` + + If no ordering was specified, the result is ordered by rowID in reverse order. + + ```swift + // SELECT * FROM players ORDER BY _rowid_ DESC + Player.all().reversed() + ``` + +- `limit(limit, offset: offset)` limits and pages results. + + ```swift + // SELECT * FROM players LIMIT 5 + Player.limit(5) + + // SELECT * FROM players LIMIT 5 OFFSET 10 + Player.limit(5, offset: 10) + ``` + +You can refine requests by chaining those methods: + +```swift +// SELECT * FROM players WHERE (email IS NOT NULL) ORDER BY name +Player.order(nameColumn).filter(emailColumn != nil) +``` + +The `select`, `order`, `group`, and `limit` methods ignore and replace previously applied selection, orderings, grouping, and limits. On the opposite, `filter`, `matching`, and `having` methods extend the query: + +```swift +Player // SELECT * FROM players + .filter(nameColumn != nil) // WHERE (name IS NOT NULL) + .filter(emailColumn != nil) // AND (email IS NOT NULL) + .order(nameColumn) // - ignored - + .order(scoreColumn) // ORDER BY score + .limit(20, offset: 40) // - ignored - + .limit(10) // LIMIT 10 +``` + + +Raw SQL snippets are also accepted, with eventual [arguments](http://groue.github.io/GRDB.swift/docs/2.0/Structs/StatementArguments.html): + +```swift +// SELECT DATE(creationDate), COUNT(*) FROM players WHERE name = 'Arthur' GROUP BY date(creationDate) +Player + .select(sql: "DATE(creationDate), COUNT(*)") + .filter(sql: "name = ?", arguments: ["Arthur"]) + .group(sql: "DATE(creationDate)") +``` + + +### Columns Selected by a Request + +By default, query interface requests select all columns: + +```swift +// SELECT * FROM players +let request = Player.all() +``` + +**The selection can be changed for each individual requests, or for all requests built from a given type.** + +To specify the selection of a specific request, use the `select` method: + +```swift +// SELECT id, name FROM players +let request = Player.select(Column("id"), Column("name")) + +// SELECT *, rowid FROM players +let request = Player.select(AllColumns(), Column.rowID) +``` + +To specify the default selection for all requests built from a type, define the `databaseSelection` property: + +```swift +struct RestrictedPlayer : TableMapping { + static let databaseTableName = "players" + static let databaseSelection: [SQLSelectable] = [Column("id"), Column("name")] +} + +struct ExtendedPlayer : TableMapping { + static let databaseTableName = "players" + static let databaseSelection: [SQLSelectable] = [AllColumns(), Column.rowID] +} + +// SELECT id, name FROM players +let request = RestrictedPlayer.all() + +// SELECT *, rowid FROM players +let request = ExtendedPlayer.all() +``` + +> :point_up: **Note**: make sure the `databaseSelection` property is explicitely declared as `[SQLSelectable]`. If it is not, the Swift compiler may infer a type which may silently miss the protocol requirement, resulting in sticky `SELECT *` requests. + + +## Expressions + +Feed [requests](#requests) with SQL expressions built from your Swift code: + + +### SQL Operators + +- `=`, `<>`, `<`, `<=`, `>`, `>=`, `IS`, `IS NOT` + + Comparison operators are based on the Swift operators `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, `>=`: + + ```swift + // SELECT * FROM players WHERE (name = 'Arthur') + Player.filter(nameColumn == "Arthur") + + // SELECT * FROM players WHERE (name IS NULL) + Player.filter(nameColumn == nil) + + // SELECT * FROM players WHERE (score IS 1000) + Player.filter(scoreColumn === 1000) + + // SELECT * FROM rectangles WHERE width < height + Rectangle.filter(widthColumn < heightColumn) + ``` + + > :point_up: **Note**: SQLite string comparison, by default, is case-sensitive and not Unicode-aware. See [string comparison](#string-comparison) if you need more control. + +- `*`, `/`, `+`, `-` + + SQLite arithmetic operators are derived from their Swift equivalent: + + ```swift + // SELECT ((temperature * 1.8) + 32) AS farenheit FROM players + Planet.select((temperatureColumn * 1.8 + 32).aliased("farenheit")) + ``` + + > :point_up: **Note**: an expression like `nameColumn + "rrr"` will be interpreted by SQLite as a numerical addition (with funny results), not as a string concatenation. + +- `AND`, `OR`, `NOT` + + The SQL logical operators are derived from the Swift `&&`, `||` and `!`: + + ```swift + // SELECT * FROM players WHERE ((NOT verified) OR (score < 1000)) + Player.filter(!verifiedColumn || scoreColumn < 1000) + ``` + +- `BETWEEN`, `IN`, `NOT IN` + + To check inclusion in a Swift sequence (array, set, range…), call the `contains` method: + + ```swift + // SELECT * FROM players WHERE id IN (1, 2, 3) + Player.filter([1, 2, 3].contains(idColumn)) + + // SELECT * FROM players WHERE id NOT IN (1, 2, 3) + Player.filter(![1, 2, 3].contains(idColumn)) + + // SELECT * FROM players WHERE score BETWEEN 0 AND 1000 + Player.filter((0...1000).contains(scoreColumn)) + + // SELECT * FROM players WHERE (score >= 0) AND (score < 1000) + Player.filter((0..<1000).contains(scoreColumn)) + + // SELECT * FROM players WHERE initial BETWEEN 'A' AND 'N' + Player.filter(("A"..."N").contains(initialColumn)) + + // SELECT * FROM players WHERE (initial >= 'A') AND (initial < 'N') + Player.filter(("A"..<"N").contains(initialColumn)) + ``` + + > :point_up: **Note**: SQLite string comparison, by default, is case-sensitive and not Unicode-aware. See [string comparison](#string-comparison) if you need more control. + +- `LIKE` + + The SQLite LIKE operator is available as the `like` method: + + ```swift + // SELECT * FROM players WHERE (email LIKE '%@example.com') + Player.filter(emailColumn.like("%@example.com")) + ``` + + > :point_up: **Note**: the SQLite LIKE operator is case-unsensitive but not Unicode-aware. For example, the expression `'a' LIKE 'A'` is true but `'æ' LIKE 'Æ'` is false. + +- `MATCH` + + The full-text MATCH operator is available through [FTS3Pattern](#fts3pattern) (for FTS3 and FTS4 tables) and [FTS5Pattern](#fts5pattern) (for FTS5): + + FTS3 and FTS4: + + ```swift + let pattern = FTS3Pattern(matchingAllTokensIn: "SQLite database") + + // SELECT * FROM documents WHERE documents MATCH 'sqlite database' + Document.matching(pattern) + + // SELECT * FROM documents WHERE content MATCH 'sqlite database' + Document.filter(contentColumn.match(pattern)) + ``` + + FTS5: + + ```swift + let pattern = FTS5Pattern(matchingAllTokensIn: "SQLite database") + + // SELECT * FROM documents WHERE documents MATCH 'sqlite database' + Document.matching(pattern) + ``` + + +### SQL Functions + +- `ABS`, `AVG`, `COUNT`, `LENGTH`, `MAX`, `MIN`, `SUM`: + + Those are based on the `abs`, `average`, `count`, `length`, `max`, `min` and `sum` Swift functions: + + ```swift + // SELECT MIN(score), MAX(score) FROM players + Player.select(min(scoreColumn), max(scoreColumn)) + + // SELECT COUNT(name) FROM players + Player.select(count(nameColumn)) + + // SELECT COUNT(DISTINCT name) FROM players + Player.select(count(distinct: nameColumn)) + ``` + +- `IFNULL` + + Use the Swift `??` operator: + + ```swift + // SELECT IFNULL(name, 'Anonymous') FROM players + Player.select(nameColumn ?? "Anonymous") + + // SELECT IFNULL(name, email) FROM players + Player.select(nameColumn ?? emailColumn) + ``` + +- `LOWER`, `UPPER` + + The query interface does not give access to those SQLite functions. Nothing against them, but they are not unicode aware. + + Instead, GRDB extends SQLite with SQL functions that call the Swift built-in string functions `capitalized`, `lowercased`, `uppercased`, `localizedCapitalized`, `localizedLowercased` and `localizedUppercased`: + + ```swift + Player.select(nameColumn.uppercased()) + ``` + + > :point_up: **Note**: When *comparing* strings, you'd rather use a [collation](#string-comparison): + > + > ```swift + > let name: String = ... + > + > // Not recommended + > nameColumn.uppercased() == name.uppercased() + > + > // Better + > nameColumn.collating(.caseInsensitiveCompare) == name + > ``` + +- Custom SQL functions and aggregates + + You can apply your own [custom SQL functions and aggregates](#custom-functions-): + + ```swift + let f = DatabaseFunction("f", ...) + + // SELECT f(name) FROM players + Player.select(f.apply(nameColumn)) + ``` + + +## Fetching from Requests + +Once you have a request, you can fetch the records at the origin of the request: + +```swift +// Some request based on `Player` +let request = Player.filter(...)... // QueryInterfaceRequest<Player> + +// Fetch players: +try request.fetchCursor(db) // A Cursor of Player +try request.fetchAll(db) // [Player] +try request.fetchOne(db) // Player? +``` + +See [fetching methods](#fetching-methods) for information about the `fetchCursor`, `fetchAll` and `fetchOne` methods. + +For example: + +```swift +let allPlayers = try Player.fetchAll(db) // [Player] +let arthur = try Player.filter(nameColumn == "Arthur").fetchOne(db) // Player? +``` + + +**When the selected columns don't fit the source type**, change your target: any other type that adopts the [RowConvertible](#rowconvertible-protocol) protocol, plain [database rows](#fetching-rows), and even [values](#values): + +```swift +let maxScore = try Player.select(max(scoreColumn)) + .asRequest(of: Int.self) + .fetchOne(db) // Int? + +let row = try Player.select(min(scoreColumn), max(scoreColumn)) + .asRequest(of: Row.self) + .fetchOne(db)! +let minScore = row[0] as Int? +let maxScore = row[1] as Int? +``` + +More information about `asRequest(of:)` can be found in the [Custom Requests](#custom-requests) chapter. + + +## Fetching By Key + +**Fetching records according to their primary key** is a very common task. It has a shortcut which accepts any single-column primary key: + +```swift +// SELECT * FROM players WHERE id = 1 +try Player.fetchOne(db, key: 1) // Player? + +// SELECT * FROM players WHERE id IN (1, 2, 3) +try Player.fetchAll(db, keys: [1, 2, 3]) // [Player] + +// SELECT * FROM players WHERE isoCode = 'FR' +try Country.fetchOne(db, key: "FR") // Country? + +// SELECT * FROM countries WHERE isoCode IN ('FR', 'US') +try Country.fetchAll(db, keys: ["FR", "US"]) // [Country] +``` + +When the table has no explicit primary key, GRDB uses the [hidden "rowid" column](#the-implicit-rowid-primary-key): + +```swift +// SELECT * FROM documents WHERE rowid = 1 +try Document.fetchOne(db, key: 1) // Document? +``` + +For multiple-column primary keys and unique keys defined by unique indexes, provide a dictionary: + +```swift +// SELECT * FROM citizenships WHERE playerID = 1 AND countryISOCode = 'FR' +try Citizenship.fetchOne(db, key: ["playerID": 1, "countryISOCode": "FR"]) // Citizenship? + +// SELECT * FROM players WHERE email = 'arthur@example.com' +try Player.fetchOne(db, key: ["email": "arthur@example.com"]) // Player? +``` + + +## Fetching Aggregated Values + +**Requests can count.** The `fetchCount()` method returns the number of rows that would be returned by a fetch request: + +```swift +// SELECT COUNT(*) FROM players +let count = try Player.fetchCount(db) // Int + +// SELECT COUNT(*) FROM players WHERE email IS NOT NULL +let count = try Player.filter(emailColumn != nil).fetchCount(db) + +// SELECT COUNT(DISTINCT name) FROM players +let count = try Player.select(nameColumn).distinct().fetchCount(db) + +// SELECT COUNT(*) FROM (SELECT DISTINCT name, score FROM players) +let count = try Player.select(nameColumn, scoreColumn).distinct().fetchCount(db) +``` + + +**Other aggregated values** can also be selected and fetched (see [SQL Functions](#sql-functions)): + +```swift +let maxScore = try Player.select(max(scoreColumn)) + .asRequest(of: Int.self) + .fetchOne(db) // Int? + +let row = try Player.select(min(scoreColumn), max(scoreColumn)) + .asRequest(of: Row.self) + .fetchOne(db)! +let minScore = row[0] as Int? +let maxScore = row[1] as Int? +``` + +More information about `asRequest(of:)` can be found in the [Custom Requests](#custom-requests) chapter. + + +## Delete Requests + +**Requests can delete records**, with the `deleteAll()` method: + +```swift +// DELETE FROM players WHERE email IS NULL +let request = Player.filter(emailColumn == nil) +try request.deleteAll(db) +``` + +> :point_up: **Note** Deletion methods are only available for records that adopts the [Persistable](#persistable-protocol) protocol. + +**Deleting records according to their primary key** is also quite common. It has a shortcut which accepts any single-column primary key: + +```swift +// DELETE FROM players WHERE id = 1 +try Player.deleteOne(db, key: 1) + +// DELETE FROM players WHERE id IN (1, 2, 3) +try Player.deleteAll(db, keys: [1, 2, 3]) + +// DELETE FROM players WHERE isoCode = 'FR' +try Country.deleteOne(db, key: "FR") + +// DELETE FROM countries WHERE isoCode IN ('FR', 'US') +try Country.deleteAll(db, keys: ["FR", "US"]) +``` + +When the table has no explicit primary key, GRDB uses the [hidden "rowid" column](#the-implicit-rowid-primary-key): + +```swift +// DELETE FROM documents WHERE rowid = 1 +try Document.deleteOne(db, key: 1) +``` + +For multiple-column primary keys and unique keys defined by unique indexes, provide a dictionary: + +```swift +// DELETE FROM citizenships WHERE playerID = 1 AND countryISOCode = 'FR' +try Citizenship.deleteOne(db, key: ["playerID": 1, "countryISOCode": "FR"]) + +// DELETE FROM players WHERE email = 'arthur@example.com' +Player.deleteOne(db, key: ["email": "arthur@example.com"]) +``` + + +## Custom Requests + +Until now, we have seen [requests](#requests) created from any type that adopts the [TableMapping](#tablemapping-protocol) protocol: + +```swift +let request = Player.all() // QueryInterfaceRequest<Player> +``` + +Those requests of type `QueryInterfaceRequest` can fetch, count, and delete records: + +```swift +try request.fetchCursor(db) // A Cursor of Player +try request.fetchAll(db) // [Player] +try request.fetchOne(db) // Player? +try request.fetchCount(db) // Int +try request.deleteAll(db) +``` + +**When the query interface can not generate the SQL you need**, you can still fallback to [raw SQL](#fetch-queries): + +```swift +// Custom SQL is always welcome +try Player.fetchAll(db, "SELECT ...") // [Player] +``` + +But you may prefer to bring some elegance back in, and build custom requests on top of the `Request` and `TypedRequest` protocols: + +```swift +// No custom SQL in sight +try Player.customRequest().fetchAll(db) // [Player] +``` + +Unlike QueryInterfaceRequest, these protocols can't delete. But they can fetch and count: + +```swift +/// The protocol for all types that define a way to fetch database rows. +protocol Request { + /// A tuple that contains a prepared statement that is ready to be + /// executed, and an eventual row adapter. + func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) + + /// The number of rows fetched by the request. + func fetchCount(_ db: Database) throws -> Int +} + +/// The protocol for requests that know how to decode database rows. +protocol TypedRequest : Request { + /// The type that can convert raw database rows to fetched values + associatedtype RowDecoder +} +``` + +The `prepare` method returns a tuple made of a [prepared statement](#prepared-statements) and an optional [row adapter](#row-adapters). The prepared statement tells which SQL query should be executed. The row adapter can help *presenting* the fetched rows in the way expected by the row consumers (we'll see an example below). + +The `fetchCount` method has a default implementation that builds a correct but naive SQL query from the statement returned by `prepare`: `SELECT COUNT(*) FROM (...)`. Adopting types can refine the counting SQL by customizing their `fetchCount` implementation. + + +### Fetching From Custom Requests + +A Request doesn't know what to fetch, but it can feed the [fetching methods](#fetching-methods) of any fetchable type ([Row](#fetching-rows), [value](#value-queries), or [record](#records)): + +```swift +let request: Request = ... +try Row.fetchCursor(db, request) // A Cursor of Row +try String.fetchAll(db, request) // [String] +try Player.fetchOne(db, request) // Player? +``` + +On top of that, a TypedRequest knows exactly what it has to do when its RowDecoder associated type can decode database rows ([Row](#fetching-rows) itself, [values](#value-queries), or [records](#records)): + +```swift +let request = ... // Some TypedRequest that fetches Player +try request.fetchCursor(db) // A Cursor of Player +try request.fetchAll(db) // [Player] +try request.fetchOne(db) // Player? +``` + + +### Building Custom Requests + +**To build custom requests**, you can create your own type that adopts the protocols, or derive requests from other requests, or use one of the built-in concrete types: + +- [SQLRequest](http://groue.github.io/GRDB.swift/docs/2.0/Structs/SQLRequest.html): a Request built from raw SQL +- [AnyRequest](http://groue.github.io/GRDB.swift/docs/2.0/Structs/AnyRequest.html): a type-erased Request +- [AnyTypedRequest](http://groue.github.io/GRDB.swift/docs/2.0/Structs/AnyTypedRequest.html): a type-erased TypedRequest + +Use the `asRequest(of:)` method to define the type fetched by the request: + +```swift +let maxScore = Player.select(max(scoreColumn)) + .asRequest(of: Int.self) + .fetchOne(db) + +extension Player { + static func customRequest(...) -> AnyTypedRequest<Player> { + let request = SQLRequest("SELECT ...", arguments: ...) + return request.asRequest(of: Player.self) + } +} + +try Player.customRequest(...).fetchAll(db) // [Player] +try Player.customRequest(...).fetchCount(db) // Int +``` + +[**:fire: EXPERIMENTAL**](#what-are-experimental-features): Use the `adapted()` method to ease the consumption of complex rows with [row adapters](#row-adapters): + +```swift +struct BookAuthorPair : RowConvertible { + let book: Book + let author: Author + + init(row: Row) { + // Those scopes are defined by the all() method below + book = Book(row: row.scoped(on: "books")!) + author = Author(row: row.scoped(on: "authors")!) + } + + static func all() -> AdaptedTypedRequest<AnyTypedRequest<BookAuthorPair>> { + return SQLRequest(""" + SELECT books.*, authors.* + FROM books + JOIN authors ON authors.id = books.authorID + """) + .asRequest(of: BookAuthorPair.self) + .adapted { db in + try ScopeAdapter([ + "books": SuffixRowAdapter(fromIndex: 0), + "authors": SuffixRowAdapter(fromIndex: db.columnCount(in: "books"))]) + } + } + + static func fetchAll(_ db: Database) throws -> [BookAuthorPair] { + return try all().fetchAll(db) + } +} + +for pair in try BookAuthorPair.fetchAll(db) { + print("\(pair.book.title) by \(pair.author.name)") +} +``` + + +Application Tools +================= + +On top of the APIs described above, GRDB provides a toolkit for applications. While none of those are mandatory, all of them help dealing with the database: + +- [Migrations](#migrations): Transform your database as your application evolves. +- [Full-Text Search](#full-text-search): Perform efficient and customizable full-text searches. +- [Database Changes Observation](#database-changes-observation): Perform post-commit and post-rollback actions. +- [FetchedRecordsController](#fetchedrecordscontroller): Automated tracking of changes in a query results, plus UITableView animations. +- [Encryption](#encryption): Encrypt your database with SQLCipher. +- [Backup](#backup): Dump the content of a database to another. + + +## Migrations + +**Migrations** are a convenient way to alter your database schema over time in a consistent and easy way. + +Migrations run in order, once and only once. When a user upgrades your application, only non-applied migrations are run. + +Inside each migration, you typically [define and update your database tables](#database-schema) according to your evolving application needs: + +```swift +var migrator = DatabaseMigrator() + +// v1 database +migrator.registerMigration("v1") { db in + try db.create(table: "players") { t in ... } + try db.create(table: "books") { t in ... } + try db.create(index: ...) +} + +// v2 database +migrator.registerMigration("v2") { db in + try db.alter(table: "players") { t in ... } +} + +// Migrations for future versions will be inserted here: +// +// // v3 database +// migrator.registerMigration("v3") { db in +// ... +// } +``` + +**Each migration runs in a separate transaction.** Should one throw an error, its transaction is rollbacked, subsequent migrations do not run, and the error is eventually thrown by `migrator.migrate(dbQueue)`. + +**The memory of applied migrations is stored in the database itself** (in a reserved table). + +You migrate the database up to the latest version with the `migrate(_:)` method: + +```swift +try migrator.migrate(dbQueue) // or migrator.migrate(dbPool) +``` + +To migrate a database up to a specific version, use `migrate(_:upTo:)`: + +```swift +try migrator.migrate(dbQueue, upTo: "v2") +``` + +Migrations can only run forward: + +```swift +try migrator.migrate(dbQueue, upTo: "v2") +try migrator.migrate(dbQueue, upTo: "v1") +// fatal error: database is already migrated beyond migration "v1" +``` + + +### Advanced Database Schema Changes + +SQLite does not support many schema changes, and won't let you drop a table column with "ALTER TABLE ... DROP COLUMN ...", for example. + +Yet any kind of schema change is still possible. The SQLite documentation explains in detail how to do so: https://www.sqlite.org/lang_altertable.html#otheralter. This technique requires the temporary disabling of foreign key checks, and is supported by the `registerMigrationWithDeferredForeignKeyCheck` function: + +```swift +// Add a NOT NULL constraint on players.name: +migrator.registerMigrationWithDeferredForeignKeyCheck("AddNotNullCheckOnName") { db in + try db.create(table: "new_players") { t in + t.column("id", .integer).primaryKey() + t.column("name", .text).notNull() + } + try db.execute("INSERT INTO new_players SELECT * FROM players") + try db.drop(table: "players") + try db.rename(table: "new_players", to: "players") +} +``` + +While your migration code runs with disabled foreign key checks, those are re-enabled and checked at the end of the migration, regardless of eventual errors. + + +## Full-Text Search + +**Full-Text Search is an efficient way to search a corpus of textual documents.** + +```swift +// Create full-text tables +try db.create(virtualTable: "books", using: FTS4()) { t in // or FTS3(), or FTS5() + t.column("author") + t.column("title") + t.column("body") +} + +// Populate full-text table with records or SQL +try Book(...).insert(db) +try db.execute( + "INSERT INTO books (author, title, body) VALUES (?, ?, ?)", + arguments: [...]) + +// Build search patterns +let pattern = FTS3Pattern(matchingPhrase: "Moby-Dick") + +// Search with the query interface or SQL +let books = try Book.matching(pattern).fetchAll(db) +let books = try Book.fetchAll(db, + "SELECT * FROM books WHERE books MATCH ?", + arguments: [pattern]) +``` + +- **[Choosing the Full-Text Engine](#choosing-the-full-text-engine)** +- **Create Full-Text Virtual Tables**: [FTS3/4](#create-fts3-and-fts4-virtual-tables), [FTS5](#create-fts5-virtual-tables) +- **Choosing a Tokenizer**: [FTS3/4](#fts3-and-fts4-tokenizers), [FTS5](#fts5-tokenizers) +- **Search Patterns**: [FTS3/4](#fts3pattern), [FTS5](#fts5pattern) +- **Sorting by Relevance**: [FTS5](#fts5-sorting-by-relevance) +- **External Content Full-Text Tables**: [FTS4/5](#external-content-full-text-tables) +- **Full-Text Record**s: [FTS3/4/5](#full-text-records) +- **Unicode Full-Text Gotchas**: [FTS3/4/5](#unicode-full-text-gotchas). Unicorns don't exist. +- **Custom Tokenizers**: [FTS5](Documentation/FTS5Tokenizers.md). Leverage extra full-text features such as synonyms or stop words. Avoid [unicode gotchas](#unicode-full-text-gotchas). +- **Sample Code**: [WWDC Companion](https://github.com/groue/WWDCCompanion), an iOS app that stores, displays, and lets the user search the WWDC 2016 sessions. + + + +### Choosing the Full-Text Engine + +**SQLite supports three full-text engines: [FTS3, FTS4](https://www.sqlite.org/fts3.html) and [FTS5](https://www.sqlite.org/fts5.html).** + +Generally speaking, FTS5 is better than FTS4 which improves on FTS3. But this does not really tell which engine to choose for your application. Instead, make your choice depend on: + +- **The full-text features needed by the application**: + + | Full-Text Needs | FTS3 | FTS4 | FTS5 | + | -------------------------------------------------------------------------- | :--: | :--: | :--: | + | :question: Queries | | | | + | **Words searches** (documents that contain "database") | X | X | X | + | **Prefix searches** (documents that contain a word starting with "data") | X | X | X | + | **Phrases searches** (documents that contain the phrase "SQLite database") | X | X | X | + | **Boolean searches** (documents that contain "SQLite" or "database") | X | X | X | + | **Proximity search** (documents that contain "SQLite" near "database") | X | X | X | + | :scissors: Tokenization | | | | + | **Ascii case insensitivity** (have "DATABASE" match "database") | X | X | X | + | **Unicode case insensitivity** (have "ÉLÉGANCE" match "élégance") | X | X | X | + | **Latin diacritics insensitivity** (have "elegance" match "élégance") | X | X | X | + | **English Stemming** (have "frustration" match "frustrated") | X | X | X | + | **English Stemming and Ascii case insensitivity** | X | X | X | + | **English Stemming and Unicode case insensitivity** | | | X | + | **English Stemming and Latin diacritics insensitivity** | | | X | + | **Synonyms** (have "1st" match "first") | ¹ | ¹ | X ² | + | **Pinyin and Romaji** (have "romaji" match "ローマ字") | ¹ | ¹ | X ² | + | **Stop words** (don't index, and don't match words like "and" and "the") | ¹ | ¹ | X ² | + | **Spell checking** (have "alamaba" match "alabama") | ¹ | ¹ | ¹ | + | :bowtie: Other Features | | | | + | **Ranking** (sort results by relevance) | ¹ | ¹ | X | + | **Snippets** (display a few words around a match) | X | X | X | + + ¹ Requires extra setup, possibly hard to implement. + + ² Requires a [custom tokenizer](Documentation/FTS5Tokenizers.md). + + For a full feature list, read the SQLite documentation. Some missing features can be achieved with extra application code. + +- **The speed versus disk space constraints.** Roughly speaking, FTS4 and FTS5 are faster than FTS3, but use more space. FTS4 only supports content compression. + +- **The location of the indexed text in your database schema.** Only FTS4 and FTS5 support "contentless" and "external content" tables. + +- **The SQLite library integrated in your application.** The version of SQLite that ships with iOS, macOS and watchOS support FTS3 and FTS4 out of the box, but not FTS5. To use FTS5, you'll need a [custom SQLite build](Documentation/CustomSQLiteBuilds.md) that activates the `SQLITE_ENABLE_FTS5` compilation option. + +- See [FST3 vs. FTS4](https://www.sqlite.org/fts3.html#differences_between_fts3_and_fts4) and [FTS5 vs. FTS3/4](https://www.sqlite.org/fts5.html#appendix_a) for more differences. + +> :point_up: **Note**: In case you were still wondering, it is recommended to read the SQLite documentation: [FTS3 & FTS4](https://www.sqlite.org/fts3.html) and [FTS5](https://www.sqlite.org/fts5.html). + + +### Create FTS3 and FTS4 Virtual Tables + +**FTS3 and FTS4 full-text tables store and index textual content.** + +Create tables with the `create(virtualTable:using:)` method: + +```swift +// CREATE VIRTUAL TABLE documents USING fts3(content) +try db.create(virtualTable: "documents", using: FTS3()) { t in + t.column("content") +} + +// CREATE VIRTUAL TABLE documents USING fts4(content) +try db.create(virtualTable: "documents", using: FTS4()) { t in + t.column("content") +} +``` + +**All columns in a full-text table contain text.** If you need to index a table that contains other kinds of values, you need an ["external content" full-text table](#external-content-full-text-tables). + +You can specify a [tokenizer](#fts3-and-fts4-tokenizers): + +```swift +// CREATE VIRTUAL TABLE books USING fts4( +// tokenize=porter, +// author, +// title, +// body +// ) +try db.create(virtualTable: "books", using: FTS4()) { t in + t.tokenizer = .porter + t.column("author") + t.column("title") + t.column("body") +} +``` + +FTS4 supports [options](https://www.sqlite.org/fts3.html#fts4_options): + +```swift +// CREATE VIRTUAL TABLE books USING fts4( +// content, +// uuid, +// content="", +// compress=zip, +// uncompress=unzip, +// prefix="2,4", +// notindexed=uuid, +// languageid=lid +// ) +try db.create(virtualTable: "documents", using: FTS4()) { t in + t.content = "" + t.compress = "zip" + t.uncompress = "unzip" + t.prefixes = [2, 4] + t.column("content") + t.column("uuid").notIndexed() + t.column("lid").asLanguageId() +} +``` + +The `content` option is involved in "contentless" and "external content" full-text tables. GRDB can help you defining full-text tables that automatically synchronize with their content table. See [External Content Full-Text Tables](#external-content-full-text-tables). + + +See [SQLite documentation](https://www.sqlite.org/fts3.html) for more information. + + +### FTS3 and FTS4 Tokenizers + +**A tokenizer defines what "matching" means.** Depending on the tokenizer you choose, full-text searches won't return the same results. + +SQLite ships with three built-in FTS3/4 tokenizers: `simple`, `porter` and `unicode61` that use different algorithms to match queries with indexed content: + +```swift +try db.create(virtualTable: "books", using: FTS4()) { t in + // Pick one: + t.tokenizer = .simple // default + t.tokenizer = .porter + t.tokenizer = .unicode61(...) +} +``` + +See below some examples of matches: + +| content | query | simple | porter | unicode61 | +| ----------- | ---------- | :----: | :----: | :-------: | +| Foo | Foo | X | X | X | +| Foo | FOO | X | X | X | +| Jérôme | Jérôme | X ¹ | X ¹ | X ¹ | +| Jérôme | JÉRÔME | | | X ¹ | +| Jérôme | Jerome | | | X ¹ | +| Database | Databases | | X | | +| Frustration | Frustrated | | X | | + +¹ Don't miss [Unicode Full-Text Gotchas](#unicode-full-text-gotchas) + +- **simple** + + ```swift + try db.create(virtualTable: "books", using: FTS4()) { t in + t.tokenizer = .simple // default + } + ``` + + The default "simple" tokenizer is case-insensitive for ASCII characters. It matches "foo" with "FOO", but not "Jérôme" with "JÉRÔME". + + It does not provide stemming, and won't match "databases" with "database". + + It does not strip diacritics from latin script characters, and won't match "jérôme" with "jerome". + +- **porter** + + ```swift + try db.create(virtualTable: "books", using: FTS4()) { t in + t.tokenizer = .porter + } + ``` + + The "porter" tokenizer compares English words according to their roots: it matches "database" with "databases", and "frustration" with "frustrated". + + It does not strip diacritics from latin script characters, and won't match "jérôme" with "jerome". + +- **unicode61** + + ```swift + try db.create(virtualTable: "books", using: FTS4()) { t in + t.tokenizer = .unicode61() + t.tokenizer = .unicode61(removeDiacritics: false) + } + ``` + + The "unicode61" tokenizer is case-insensitive for unicode characters. It matches "Jérôme" with "JÉRÔME". + + It strips diacritics from latin script characters by default, and matches "jérôme" with "jerome". This behavior can be disabled, as in the example above. + + It does not provide stemming, and won't match "databases" with "database". + +See [SQLite tokenizers](https://www.sqlite.org/fts3.html#tokenizer) for more information. + + +### FTS3Pattern + +**Full-text search in FTS3 and FTS4 tables is performed with search patterns:** + +- `database` matches all documents that contain "database" +- `data*` matches all documents that contain a word starting with "data" +- `SQLite database` matches all documents that contain both "SQLite" and "database" +- `SQLite OR database` matches all documents that contain "SQLite" or "database" +- `"SQLite database"` matches all documents that contain the "SQLite database" phrase + +**Not all search patterns are valid**: they must follow the [Full-Text Index Queries Grammar](https://www.sqlite.org/fts3.html#full_text_index_queries). + +The FTS3Pattern type helps you validating patterns, and building valid patterns from untrusted strings (such as strings typed by users): + +```swift +struct FTS3Pattern { + init(rawPattern: String) throws + init?(matchingAnyTokenIn string: String) + init?(matchingAllTokensIn string: String) + init?(matchingPhrase string: String) +} +``` + +The first initializer validates your raw patterns against the query grammar, and may throw a [DatabaseError](#databaseerror): + +```swift +// OK: FTS3Pattern +let pattern = try FTS3Pattern(rawPattern: "sqlite AND database") +// DatabaseError: malformed MATCH expression: [AND] +let pattern = try FTS3Pattern(rawPattern: "AND") +``` + +The three other initializers don't throw. They build a valid pattern from any string, **including strings provided by users of your application**. They let you find documents that match all given words, any given word, or a full phrase, depending on the needs of your application: + +```swift +let query = "SQLite database" +// Matches documents that contain "SQLite" or "database" +let pattern = FTS3Pattern(matchingAnyTokenIn: query) +// Matches documents that contain both "SQLite" and "database" +let pattern = FTS3Pattern(matchingAllTokensIn: query) +// Matches documents that contain "SQLite database" +let pattern = FTS3Pattern(matchingPhrase: query) +``` + +They return nil when no pattern could be built from the input string: + +```swift +let pattern = FTS3Pattern(matchingAnyTokenIn: "") // nil +let pattern = FTS3Pattern(matchingAnyTokenIn: "*") // nil +``` + +FTS3Pattern are regular [values](#values). You can use them as query [arguments](http://groue.github.io/GRDB.swift/docs/2.0/Structs/StatementArguments.html): + +```swift +let documents = try Document.fetchAll(db, + "SELECT * FROM documents WHERE content MATCH ?", + arguments: [pattern]) +``` + +Use them in the [query interface](#the-query-interface): + +```swift +// Search in all columns +let documents = try Document.matching(pattern).fetchAll(db) + +// Search in a specific column: +let documents = try Document.filter(Column("content").match(pattern)).fetchAll(db) +``` + + +### Create FTS5 Virtual Tables + +**FTS5 full-text tables store and index textual content.** + +To use FTS5, you'll need a [custom SQLite build](Documentation/CustomSQLiteBuilds.md) that activates the `SQLITE_ENABLE_FTS5` compilation option. + +Create FTS5 tables with the `create(virtualTable:using:)` method: + +```swift +// CREATE VIRTUAL TABLE documents USING fts5(content) +try db.create(virtualTable: "documents", using: FTS5()) { t in + t.column("content") +} +``` + +**All columns in a full-text table contain text.** If you need to index a table that contains other kinds of values, you need an ["external content" full-text table](#external-content-full-text-tables). + +You can specify a [tokenizer](#fts5-tokenizers): + +```swift +// CREATE VIRTUAL TABLE books USING fts5( +// tokenize='porter', +// author, +// title, +// body +// ) +try db.create(virtualTable: "books", using: FTS5()) { t in + t.tokenizer = .porter() + t.column("author") + t.column("title") + t.column("body") +} +``` + +FTS5 supports [options](https://www.sqlite.org/fts5.html#fts5_table_creation_and_initialization): + +```swift +// CREATE VIRTUAL TABLE books USING fts5( +// content, +// uuid UNINDEXED, +// content='table', +// content_rowid='id', +// prefix='2 4', +// columnsize=0, +// detail=column +// ) +try db.create(virtualTable: "documents", using: FTS5()) { t in + t.column("content") + t.column("uuid").notIndexed() + t.content = "table" + t.contentRowID = "id" + t.prefixes = [2, 4] + t.columnSize = 0 + t.detail = "column" +} +``` + +The `content` and `contentRowID` options are involved in "contentless" and "external content" full-text tables. GRDB can help you defining full-text tables that automatically synchronize with their content table. See [External Content Full-Text Tables](#external-content-full-text-tables). + +See [SQLite documentation](https://www.sqlite.org/fts5.html) for more information. + + +### FTS5 Tokenizers + +**A tokenizer defines what "matching" means.** Depending on the tokenizer you choose, full-text searches won't return the same results. + +SQLite ships with three built-in FTS5 tokenizers: `ascii`, `porter` and `unicode61` that use different algorithms to match queries with indexed content. + +```swift +try db.create(virtualTable: "books", using: FTS5()) { t in + // Pick one: + t.tokenizer = .unicode61() // default + t.tokenizer = .unicode61(...) + t.tokenizer = .ascii + t.tokenizer = .porter(...) +} +``` + +See below some examples of matches: + +| content | query | ascii | unicode61 | porter on ascii | porter on unicode61 | +| ----------- | ---------- | :----: | :-------: | :-------------: | :-----------------: | +| Foo | Foo | X | X | X | X | +| Foo | FOO | X | X | X | X | +| Jérôme | Jérôme | X ¹ | X ¹ | X ¹ | X ¹ | +| Jérôme | JÉRÔME | | X ¹ | | X ¹ | +| Jérôme | Jerome | | X ¹ | | X ¹ | +| Database | Databases | | | X | X | +| Frustration | Frustrated | | | X | X | + +¹ Don't miss [Unicode Full-Text Gotchas](#unicode-full-text-gotchas) + +- **unicode61** + + ```swift + try db.create(virtualTable: "books", using: FTS5()) { t in + t.tokenizer = .unicode61() + t.tokenizer = .unicode61(removeDiacritics: false) + } + ``` + + The default "unicode61" tokenizer is case-insensitive for unicode characters. It matches "Jérôme" with "JÉRÔME". + + It strips diacritics from latin script characters by default, and matches "jérôme" with "jerome". This behavior can be disabled, as in the example above. + + It does not provide stemming, and won't match "databases" with "database". + +- **ascii** + + ```swift + try db.create(virtualTable: "books", using: FTS5()) { t in + t.tokenizer = .ascii + } + ``` + + The "ascii" tokenizer is case-insensitive for ASCII characters. It matches "foo" with "FOO", but not "Jérôme" with "JÉRÔME". + + It does not provide stemming, and won't match "databases" with "database". + + It does not strip diacritics from latin script characters, and won't match "jérôme" with "jerome". + +- **porter** + + ```swift + try db.create(virtualTable: "books", using: FTS5()) { t in + t.tokenizer = .porter() // porter wrapping unicode61 (the default) + t.tokenizer = .porter(.ascii) // porter wrapping ascii + t.tokenizer = .porter(.unicode61(removeDiacritics: false)) // porter wrapping unicode61 without diacritics stripping + } + ``` + + The porter tokenizer is a wrapper tokenizer which compares English words according to their roots: it matches "database" with "databases", and "frustration" with "frustrated". + + It strips diacritics from latin script characters if it wraps unicode61, and does not if it wraps ascii (see the example above). + +See [SQLite tokenizers](https://www.sqlite.org/fts5.html#tokenizers) for more information, and [custom FTS5 tokenizers](Documentation/FTS5Tokenizers.md) in order to add your own tokenizers. + + +### FTS5Pattern + +**Full-text search in FTS5 tables is performed with search patterns:** + +- `database` matches all documents that contain "database" +- `data*` matches all documents that contain a word starting with "data" +- `SQLite database` matches all documents that contain both "SQLite" and "database" +- `SQLite OR database` matches all documents that contain "SQLite" or "database" +- `"SQLite database"` matches all documents that contain the "SQLite database" phrase + +**Not all search patterns are valid**: they must follow the [Full-Text Query Syntax](https://www.sqlite.org/fts5.html#full_text_query_syntax). + +The FTS5Pattern type helps you validating patterns, and building valid patterns from untrusted strings (such as strings typed by users): + +```swift +extension Database { + func makeFTS5Pattern(rawPattern: String, forTable table: String) throws -> FTS5Pattern +} + +struct FTS5Pattern { + init?(matchingAnyTokenIn string: String) + init?(matchingAllTokensIn string: String) + init?(matchingPhrase string: String) +} +``` + +The `Database.makeFTS5Pattern(rawPattern:forTable:)` method validates your raw patterns against the query grammar and the columns of the targeted table, and may throw a [DatabaseError](#databaseerror): + +```swift +// OK: FTS5Pattern +try db.makeFTS5Pattern(rawPattern: "sqlite", forTable: "books") +// DatabaseError: syntax error near \"AND\" +try db.makeFTS5Pattern(rawPattern: "AND", forTable: "books") +// DatabaseError: no such column: missing +try db.makeFTS5Pattern(rawPattern: "missing: sqlite", forTable: "books") +``` + +The FTS5Pattern initializers don't throw. They build a valid pattern from any string, **including strings provided by users of your application**. They let you find documents that match all given words, any given word, or a full phrase, depending on the needs of your application: + +```swift +let query = "SQLite database" +// Matches documents that contain "SQLite" or "database" +let pattern = FTS5Pattern(matchingAnyTokenIn: query) +// Matches documents that contain both "SQLite" and "database" +let pattern = FTS5Pattern(matchingAllTokensIn: query) +// Matches documents that contain "SQLite database" +let pattern = FTS5Pattern(matchingPhrase: query) +``` + +They return nil when no pattern could be built from the input string: + +```swift +let pattern = FTS5Pattern(matchingAnyTokenIn: "") // nil +let pattern = FTS5Pattern(matchingAnyTokenIn: "*") // nil +``` + +FTS5Pattern are regular [values](#values). You can use them as query [arguments](http://groue.github.io/GRDB.swift/docs/2.0/Structs/StatementArguments.html): + +```swift +let documents = try Document.fetchAll(db, + "SELECT * FROM documents WHERE documents MATCH ?", + arguments: [pattern]) +``` + +Use them in the [query interface](#the-query-interface): + +```swift +let documents = try Document.matching(pattern).fetchAll(db) +``` + + +### FTS5: Sorting by Relevance + +**FTS5 can sort results by relevance** (most to least relevant): + +```swift +// SQL +let documents = try Document.fetchAll(db, + "SELECT * FROM documents WHERE documents MATCH ? ORDER BY rank", + arguments: [pattern]) + +// Query Interface +let documents = try Document.matching(pattern).order(Column.rank).fetchAll(db) +``` + +For more information about the ranking algorithm, as well as extra options, read [Sorting by Auxiliary Function Results](https://www.sqlite.org/fts5.html#sorting_by_auxiliary_function_results) + +GRDB does not provide any ranking for FTS3 and FTS4. See SQLite's [Search Application Tips](https://www.sqlite.org/fts3.html#appendix_a) if you really need it. + + +### External Content Full-Text Tables + +**An external content table does not store the indexed text.** Instead, it indexes the text stored in another table. + +This is very handy when you want to index a table that can not be declared as a full-text table (because it contains non-textual values, for example). You just have to define an external content full-text table that refers to the regular table. + +The two tables must be kept up-to-date, so that the full-text index matches the content of the regular table. This synchronization happens automatically if you use the `synchronize(withTable:)` method in your full-text table definition: + +```swift +// A regular table +try db.create(table: "books") { t in + t.column("author", .text) + t.column("title", .text) + t.column("content", .text) + ... +} + +// A full-text table synchronized with the regular table +try db.create(virtualTable: "books_ft", using: FTS4()) { t in // or FTS5() + t.synchronize(withTable: "books") + t.column("author") + t.column("title") + t.column("content") +} +``` + +The eventual content already present in the regular table is indexed, and every insert, update or delete that happens in the regular table is automatically applied to the full-text index by the mean of SQL triggers. + +For more information, see the SQLite documentation about external content tables: [FTS4](https://www.sqlite.org/fts3.html#_external_content_fts4_tables_), [FTS5](https://sqlite.org/fts5.html#external_content_tables). + +See also [WWDC Companion](https://github.com/groue/WWDCCompanion), a sample app that uses external content tables to store, display, and let the user search the WWDC 2016 sessions. + + +#### Querying External Content Full-Text Tables + +When you need to perform a full-text search, and the external content table contains all the data you need, you can simply query the full-text table. + +But if you need to load columns from the regular table, and in the same time perform a full-text search, then you will need to query both tables at the same time. + +That is because SQLite will throw an error when you try to perform a full-text search on a regular table: + +```swift +// SQLite error 1: unable to use function MATCH in the requested context +// SELECT * FROM books WHERE books MATCH '...' +let books = Book.matching(pattern).fetchAll(db) +``` + +The solution is to perform a joined request, using raw SQL: + +```swift +let sql = """ + SELECT books.* + FROM books + JOIN books_ft ON + books_ft.rowid = books.rowid AND + books_ft MATCH ? + """ +let books = Book.fetchAll(db, sql, arguments: [pattern]) +``` + + +### Full-Text Records + +**You can define [record](#records) types around the full-text virtual tables.** + +However these tables don't have any explicit primary key. Instead, they use the [implicit rowid primary key](#the-implicit-rowid-primary-key): a special hidden column named `rowid`. + +You will have to [expose this hidden column](#exposing-the-rowid-column) in order to fetch, delete, and update full-text records by primary key. + + +### Unicode Full-Text Gotchas + +The SQLite built-in tokenizers for [FTS3, FTS4](#fts3-and-fts4-tokenizers) and [FTS5](#fts5-tokenizers) are generally unicode-aware, with a few caveats, and limitations. + +Generally speaking, matches may fail when content and query don't use the same [unicode normalization](http://unicode.org/reports/tr15/). SQLite actually exhibits inconsistent behavior in this regard. + +For example, for "aimé" to match "aimé", they better have the same normalization: the NFC "aim\u{00E9}" form may not match its NFD "aime\u{0301}" equivalent. Most strings that you get from Swift, UIKit and Cocoa use NFC, so be careful with NFD inputs (such as strings from the HFS+ file system, or strings that you can't trust like network inputs). Use [String.precomposedStringWithCanonicalMapping](https://developer.apple.com/reference/swift/string/1407210-precomposedstringwithcanonicalma) to turn a string into NFC. + +Besides, if you want "fi" to match the ligature "fi" (U+FB01), then you need to normalize your indexed contents and inputs to NFKC or NFKD. Use [String.precomposedStringWithCompatibilityMapping](https://developer.apple.com/reference/swift/string/1407834-precomposedstringwithcompatibili) to turn a string into NFKC. + +Unicode normalization is not the end of the story, because it won't help "Encyclopaedia" match "Encyclopædia", "Mueller", "Müller", "Grossmann", "Großmann", or "Diyarbakır", "DIYARBAKIR". The [String.applyingTransform](https://developer.apple.com/reference/swift/string/1643133-applyingtransform) method can help. + +GRDB lets you write [custom FTS5 tokenizers](Documentation/FTS5Tokenizers.md) that can transparently deal with all these issues. For FTS3 and FTS4, you'll need to pre-process your strings before injecting them in the full-text engine. + +Happy indexing! + + +## Database Changes Observation + +**SQLite notifies its host application of changes performed to the database, as well of transaction commits and rollbacks.** + +GRDB puts this SQLite feature to some good use, and lets you observe the database in various ways: + +- [After Commit Hook](#after-commit-hook): The simplest way to handle successful transactions. +- [TransactionObserver Protocol](#transactionobserver-protocol): The low-level protocol for database observation + - [Activate a Transaction Observer](#activate-a-transaction-observer) + - [Database Changes And Transactions](#database-changes-and-transactions) + - [Filtering Database Events](#filtering-database-events) + - [Observation Extent](#observation-extent) + - [Support for SQLite Pre-Update Hooks](#support-for-sqlite-pre-update-hooks) +- [FetchedRecordsController](#fetchedrecordscontroller): Automated tracking of changes in a query results, plus UITableView animations +- [RxGRDB](http://github.com/RxSwiftCommunity/RxGRDB): Automated tracking of changes in a query results, based on [RxSwift](https://github.com/ReactiveX/RxSwift) + +Database observation requires that a single [database queue](#database-queues) or [pool](#database-pools) is kept open for all the duration of the database usage. + + +### After Commit Hook + +**When your application needs to make sure a database transaction has been successfully committed before it executes some work, use the `Database.afterNextTransactionCommit(_:)` method.** + +Its closure argument is called right after database changes have been successfully written to disk: + +```swift +dbQueue.inTransaction { db in + db.afterNextTransactionCommit { db in + print("success") + } + ... + return .commit // prints "success" +} +``` + +The closure runs in a protected dispatch queue, serialized with all database updates. + +**This "after commit hook" helps synchronizing the database with other resources, such as files, or system sensors.** + +In the example below, a [location manager](https://developer.apple.com/documentation/corelocation/cllocationmanager) starts monitoring a CLRegion if and only if it has successfully been stored in the database: + +```swift +/// Inserts a region in the database, and start monitoring upon +/// successful insertion. +func startMonitoring(_ db: Database, region: CLRegion) throws { + // Make sure database is inside a transaction + try db.inSavepoint { + + // Save the region in the database + try insert(...) + + // Start monitoring if and only if the insertion is + // eventually committed + db.afterNextTransactionCommit { _ in + // locationManager prefers the main queue: + DispatchQueue.main.async { + locationManager.startMonitoring(for: region) + } + } + + return .commit + } +} +``` + +The method above won't trigger the location manager if the transaction is eventually rollbacked (explicitely, or because of an error), as in the sample code below: + +```swift +try dbQueue.inTransaction { db in + // success + try startMonitoring(db, region) + + // On error, the transaction is rollbacked, the region is not inserted, and + // the location manager is not invoked. + try failableMethod(db) + + return .commit +} +``` + + +### TransactionObserver Protocol + +The `TransactionObserver` protocol lets you **observe database changes and transactions**: + +```swift +protocol TransactionObserver : class { + /// Filters database changes that should be notified the the + /// `databaseDidChange(with:)` method. + func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool + + /// Notifies a database change: + /// - event.kind (insert, update, or delete) + /// - event.tableName + /// - event.rowID + /// + /// For performance reasons, the event is only valid for the duration of + /// this method call. If you need to keep it longer, store a copy: + /// event.copy(). + func databaseDidChange(with event: DatabaseEvent) + + /// An opportunity to rollback pending changes by throwing an error. + func databaseWillCommit() throws + + /// Database changes have been committed. + func databaseDidCommit(_ db: Database) + + /// Database changes have been rollbacked. + func databaseDidRollback(_ db: Database) +} +``` + +- [Activate a Transaction Observer](#activate-a-transaction-observer) +- [Database Changes And Transactions](#database-changes-and-transactions) +- [Filtering Database Events](#filtering-database-events) +- [Observation Extent](#observation-extent) +- [Support for SQLite Pre-Update Hooks](#support-for-sqlite-pre-update-hooks) + + +#### Activate a Transaction Observer + +**To activate a transaction observer, add it to the database queue or pool:** + +```swift +let observer = MyObserver() +dbQueue.add(transactionObserver: observer) +``` + +By default, database holds weak references to its transaction observers: they are not retained, and stop getting notifications after they are deallocated. See [Observation Extent](#observation-extent) for more options. + + +#### Database Changes And Transactions + +**A transaction observer is notified of all database changes**: inserts, updates and deletes. This includes indirect changes triggered by ON DELETE and ON UPDATE actions associated to [foreign keys](https://www.sqlite.org/foreignkeys.html#fk_actions). + +> :point_up: **Note**: the changes that are not notified are changes to internal system tables (such as `sqlite_master`), changes to [`WITHOUT ROWID`](https://www.sqlite.org/withoutrowid.html) tables, and the deletion of duplicate rows triggered by [`ON CONFLICT REPLACE`](https://www.sqlite.org/lang_conflict.html) clauses (this last exception might change in a future release of SQLite). + +Notified changes are not actually written to disk until `databaseDidCommit` is called. On the other side, `databaseDidRollback` confirms their invalidation: + +```swift +try dbQueue.inTransaction { db in + try db.execute("INSERT ...") // 1. didChange + try db.execute("UPDATE ...") // 2. didChange + return .commit // 3. willCommit, 4. didCommit +} + +try dbQueue.inTransaction { db in + try db.execute("INSERT ...") // 1. didChange + try db.execute("UPDATE ...") // 2. didChange + return .rollback // 3. didRollback +} +``` + +Database statements that are executed outside of an explicit transaction do not drop off the radar: + +```swift +try dbQueue.inDatabase { db in + try db.execute("INSERT ...") // 1. didChange, 2. willCommit, 3. didCommit + try db.execute("UPDATE ...") // 4. didChange, 5. willCommit, 6. didCommit +} +``` + +Changes that are on hold because of a [savepoint](https://www.sqlite.org/lang_savepoint.html) are only notified after the savepoint has been released. This makes sure that notified events are only events that have an opportunity to be committed: + +```swift +try dbQueue.inTransaction { db in + try db.execute("INSERT ...") // 1. didChange + + try db.execute("SAVEPOINT foo") + try db.execute("UPDATE ...") // delayed + try db.execute("UPDATE ...") // delayed + try db.execute("RELEASE SAVEPOINT foo") // 2. didChange, 3. didChange + + try db.execute("SAVEPOINT foo") + try db.execute("UPDATE ...") // not notified + try db.execute("ROLLBACK TO SAVEPOINT foo") + + return .commit // 4. willCommit, 5. didCommit +} +``` + + +**Eventual errors** thrown from `databaseWillCommit` are exposed to the application code: + +```swift +do { + try dbQueue.inTransaction { db in + ... + return .commit // 1. willCommit (throws), 2. didRollback + } +} catch { + // 3. The error thrown by the transaction observer. +} +``` + +> :point_up: **Note**: all callbacks are called in a protected dispatch queue, and serialized with all database updates. +> +> :point_up: **Note**: the databaseDidChange(with:) and databaseWillCommit() callbacks must not touch the SQLite database. This limitation does not apply to databaseDidCommit and databaseDidRollback which can use their database argument. + +[FetchedRecordsController](#fetchedrecordscontroller) and [RxGRDB](http://github.com/RxSwiftCommunity/RxGRDB) are based on the TransactionObserver protocol. + +See also [TableChangeObserver.swift](https://gist.github.com/groue/2e21172719e634657dfd), which shows a transaction observer that notifies of modified database tables with NSNotificationCenter. + + +#### Filtering Database Events + +**Transaction observers can avoid being notified of database changes they are not interested in.** + +The filtering happens in the `observes(eventsOfKind:)` method, which tells whether the observer wants notification of specific kinds of changes, or not. For example, here is how an observer can focus on the changes that happen on the "players" database table: + +```swift +class PlayerObserver: TransactionObserver { + func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { + // Only observe changes to the "players" table. + return eventKind.tableName == "players" + } + + func databaseDidChange(with event: DatabaseEvent) { + // This method is only called for changes that happen to + // the "players" table. + } +} +``` + +Generally speaking, the `observes(eventsOfKind:)` method can distinguish insertions from deletions and updates, and is also able to inspect the columns that are about to be changed: + +```swift +class PlayerScoreObserver: TransactionObserver { + func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { + // Only observe changes to the "score" column of the "players" table. + switch eventKind { + case .insert(let tableName): + return tableName == "players" + case .delete(let tableName): + return tableName == "players" + case .update(let tableName, let columnNames): + return tableName == "players" && columnNames.contains("score") + } + } +} +``` + +When the `observes(eventsOfKind:)` method returns false for all event kinds, the observer is still notified of commits and rollbacks: + +```swift +class PureTransactionObserver: TransactionObserver { + func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { + // Ignore all individual changes + return false + } + + func databaseDidChange(with event: DatabaseEvent) { /* Never called */ } + func databaseWillCommit() throws { /* Called before commit */ } + func databaseDidRollback(_ db: Database) { /* Called on rollback */ } + func databaseDidCommit(_ db: Database) { /* Called on commit */ } +} +``` + + +#### Observation Extent + +**You can specify how long an observer is notified of database changes and transactions.** + +The `remove(transactionObserver:)` method explicitely stops notifications, at any time: + +```swift +// From a database queue or pool: +dbQueue.remove(transactionObserver: observer) + +// From a database connection: +dbQueue.inDatabase { db in // or dbPool.write + db.remove(transactionObserver: observer) +} +``` + +Alternatively, use the `extent` parameter of the `add(transactionObserver:extent:)` method: + +```swift +let observer = MyObserver() + +// On a database queue or pool: +dbQueue.add(transactionObserver: observer) // default extent +dbQueue.add(transactionObserver: observer, extent: .observerLifetime) +dbQueue.add(transactionObserver: observer, extent: .nextTransaction) +dbQueue.add(transactionObserver: observer, extent: .databaseLifetime) + +// On a database connection: +dbQueue.inDatabase { db in + db.add(transactionObserver: ...) +} +``` + +- The default extent is `.observerLifetime`: the database holds a weak reference to the observer, and the observation automatically ends when the observer is deallocated. Meanwhile, observer is notified of all changes and transactions. + +- `.nextTransaction` activates the observer until the current or next transaction completes. The database keeps a strong reference to the observer until its `databaseDidCommit` or `databaseDidRollback` method is eventually called. Hereafter the observer won't get any further notification. + +- `.databaseLifetime` has the database retain and notify the observer until the database connection is closed. + + +#### Support for SQLite Pre-Update Hooks + +A [custom SQLite build](Documentation/CustomSQLiteBuilds.md) can activate [SQLite "preupdate hooks"](http://www.sqlite.org/sessions/c3ref/preupdate_count.html). In this case, TransactionObserverType gets an extra callback which lets you observe individual column values in the rows modified by a transaction: + +```swift +protocol TransactionObserverType : class { + #if SQLITE_ENABLE_PREUPDATE_HOOK + /// Notifies before a database change (insert, update, or delete) + /// with change information (initial / final values for the row's + /// columns). + /// + /// The event is only valid for the duration of this method call. If you + /// need to keep it longer, store a copy: event.copy(). + func databaseWillChange(with event: DatabasePreUpdateEvent) + #endif +} +``` + + +## FetchedRecordsController + +**You use FetchedRecordsController to track changes in the results of an SQLite request.** + +**FetchedRecordsController can also feed table views, collection views, and animate cells when the results of the request change.** + +It looks and behaves very much like [Core Data's NSFetchedResultsController](https://developer.apple.com/library/ios/documentation/CoreData/Reference/NSFetchedResultsController_Class/). + +Given a fetch request, and a type that adopts the [RowConvertible](#rowconvertible-protocol) protocol, such as a subclass of the [Record](#record-class) class, a FetchedRecordsController is able to track changes in the results of the fetch request, notify of those changes, and return the results of the request in a form that is suitable for a table view or a collection view, with one cell per fetched record. + +See [GRDBDemoiOS](DemoApps/GRDBDemoiOS/GRDBDemoiOS) for an sample app that uses FetchedRecordsController. + +See also [RxGRDB](http://github.com/RxSwiftCommunity/RxGRDB), an [RxSwift](https://github.com/ReactiveX/RxSwift) extension, for a reactive way to track request changes. + +- [Creating the Fetched Records Controller](#creating-the-fetched-records-controller) +- [Responding to Changes](#responding-to-changes) +- [The Changes Notifications](#the-changes-notifications) +- [Modifying the Fetch Request](#modifying-the-fetch-request) +- [Table and Collection Views](#table-and-collection-views) + - [Implementing the Table View Datasource Methods](#implementing-the-table-view-datasource methods) + - [Implementing Table View Updates](#implementing-table-view-updates) +- [FetchedRecordsController Concurrency](#fetchedrecordscontroller-concurrency) + + +### Creating the Fetched Records Controller + +When you initialize a fetched records controller, you provide the following mandatory information: + +- A [database connection](#database-connections) +- The type of the fetched records. It must be a type that adopts the [RowConvertible](#rowconvertible-protocol) protocol, such as a subclass of the [Record](#record-class) class +- A fetch request + +```swift +class Player : Record { ... } +let dbQueue = DatabaseQueue(...) // Or DatabasePool + +// Using a Request from the Query Interface: +let controller = FetchedRecordsController( + dbQueue, + request: Player.order(Column("name"))) + +// Using SQL, and eventual arguments: +let controller = FetchedRecordsController<Player>( + dbQueue, + sql: "SELECT * FROM players ORDER BY name WHERE countryIsoCode = ?", + arguments: ["FR"]) +``` + +The fetch request can involve several database tables. The fetched records controller will only track changes in the columns and tables used by the fetch request. + +```swift +let controller = FetchedRecordsController<Player>( + dbQueue, + sql: """ + SELECT players.name, COUNT(books.id) AS bookCount + FROM players + LEFT JOIN books ON books.authorId = players.id + GROUP BY players.id + ORDER BY players.name + """) +``` + + +After creating an instance, you invoke `performFetch()` to actually execute +the fetch. + +```swift +try controller.performFetch() +``` + + +### Responding to Changes + +In general, FetchedRecordsController is designed to respond to changes at *the database layer*, by [notifying](#the-changes-notifications) when *database rows* change location or values. + +Changes are not reflected until they are applied in the database by a successful [transaction](#transactions-and-savepoints). Transactions can be explicit, or implicit: + +```swift +try dbQueue.inTransaction { db in + try player1.insert(db) + try player2.insert(db) + return .commit // Explicit transaction +} + +try dbQueue.inDatabase { db in + try player1.insert(db) // Implicit transaction + try player2.insert(db) // Implicit transaction +} +``` + +When you apply several changes to the database, you should group them in a single explicit transaction. The controller will then notify of all changes together. + + +### The Changes Notifications + +An instance of FetchedRecordsController notifies that the controller’s fetched records have been changed by the mean of *callbacks*: + +```swift +let controller = try FetchedRecordsController(...) + +controller.trackChanges( + // controller's records are about to change: + willChange: { controller in ... }, + + // notification of individual record changes: + onChange: { (controller, record, change) in ... }, + + // controller's records have changed: + didChange: { controller in ... }) + +try controller.performFetch() +``` + +See [Implementing Table View Updates](#implementing-table-view-updates) for more detail on table view updates. + +**All callbacks are optional.** When you only need to grab the latest results, you can omit the `didChange` argument name: + +```swift +controller.trackChanges { controller in + let newPlayers = controller.fetchedRecords // [Player] +} +``` + +Callbacks have the fetched record controller itself as an argument: use it in order to avoid memory leaks: + +```swift +// BAD: memory leak +controller.trackChanges { _ in + let newPlayers = controller.fetchedRecords +} + +// GOOD +controller.trackChanges { controller in + let newPlayers = controller.fetchedRecords +} +``` + +**Callbacks are invoked asynchronously.** See [FetchedRecordsController Concurrency](#fetchedrecordscontroller-concurrency) for more information. + +**Values fetched from inside callbacks may be inconsistent with the controller's records.** This is because after database has changed, and before the controller had the opportunity to invoke callbacks in the main thread, other database changes can happen. + +To avoid inconsistencies, provide a `fetchAlongside` argument to the `trackChanges` method, as below: + +```swift +controller.trackChanges( + fetchAlongside: { db in + // Fetch any extra value, for example the number of fetched records: + return try Player.fetchCount(db) + }, + didChange: { (controller, count) in + // The extra value is the second argument. + let recordsCount = controller.fetchedRecords.count + assert(count == recordsCount) // guaranteed + }) +``` + +Whenever the fetched records controller can not look for changes after a transaction has potentially modified the tracked request, an error handler is called. The request observation is not stopped, though: future transactions may successfully be handled, and the notified changes will then be based on the last successful fetch. + +```swift +controller.trackErrors { (controller, error) in + print("Missed a transaction because \(error)") +} +``` + + +### Modifying the Fetch Request + +You can change a fetched records controller's fetch request or SQL query. + +```swift +controller.setRequest(Player.order(Column("name"))) +controller.setRequest(sql: "SELECT ...", arguments: ...) +``` + +The [notification callbacks](#the-changes-notifications) are notified of eventual changes if the new request fetches a different set of records. + +> :point_up: **Note**: This behavior differs from Core Data's NSFetchedResultsController, which does not notify of record changes when the fetch request is replaced. + +**Change callbacks are invoked asynchronously.** This means that modifying the request from the main thread does *not* immediately triggers callbacks. When you need to take immediate action, force the controller to refresh immediately with its `performFetch` method. In this case, changes callbacks are *not* called: + +```swift +// Change request on the main thread: +controller.setRequest(Player.order(Column("name"))) +// Here callbacks have not been called yet. +// You can cancel them, and refresh records immediately: +try controller.performFetch() +``` + +### Table and Collection Views + +FetchedRecordsController let you feed table and collection views, and keep them up-to-date with the database content. + +For nice animated updates, a fetched records controller needs to recognize identical records between two different result sets. When records adopt the [TableMapping](#tablemapping-protocol) protocol, they are automatically compared according to their primary key: + +```swift +class Player : TableMapping { ... } +let controller = FetchedRecordsController( + dbQueue, + request: Player.all()) +``` + +For other types, the fetched records controller needs you to be more explicit: + +```swift +let controller = FetchedRecordsController( + dbQueue, + request: ..., + isSameRecord: { (player1, player2) in player1.id == player2.id }) +``` + + +#### Implementing the Table View Datasource Methods + +The table view data source asks the fetched records controller to provide relevant information: + +```swift +func numberOfSections(in tableView: UITableView) -> Int { + return fetchedRecordsController.sections.count +} + +func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return fetchedRecordsController.sections[section].numberOfRecords +} + +func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = ... + let record = fetchedRecordsController.record(at: indexPath) + // Configure the cell + return cell +} +``` + +> :point_up: **Note**: In its current state, FetchedRecordsController does not support grouping table view rows into custom sections: it generates a unique section. + + +#### Implementing Table View Updates + +When changes in the fetched records should reload the whole table view, you can simply tell so: + +```swift +controller.trackChanges { [unowned self] _ in + self.tableView.reloadData() +} +``` + +Yet, FetchedRecordsController can notify that the controller’s fetched records have been changed due to some add, remove, move, or update operations, and help applying animated changes to a UITableView. + + +##### Typical Table View Updates + +For animated table view updates, use the `willChange` and `didChange` callbacks to bracket events provided by the fetched records controller, as illustrated in the following example: + +```swift +// Assume self has a tableView property, and a cell configuration +// method named configure(_:at:). + +controller.trackChanges( + // controller's records are about to change: + willChange: { [unowned self] _ in + self.tableView.beginUpdates() + }, + + // notification of individual record changes: + onChange: { [unowned self] (controller, record, change) in + switch change { + case .insertion(let indexPath): + self.tableView.insertRows(at: [indexPath], with: .fade) + + case .deletion(let indexPath): + self.tableView.deleteRows(at: [indexPath], with: .fade) + + case .update(let indexPath, _): + if let cell = self.tableView.cellForRow(at: indexPath) { + self.configure(cell, at: indexPath) + } + + case .move(let indexPath, let newIndexPath, _): + self.tableView.deleteRows(at: [indexPath], with: .fade) + self.tableView.insertRows(at: [newIndexPath], with: .fade) + + // // Alternate technique which actually moves cells around: + // let cell = self.tableView.cellForRow(at: indexPath) + // self.tableView.moveRow(at: indexPath, to: newIndexPath) + // if let cell = cell { + // self.configure(cell, at: newIndexPath) + // } + } + }, + + // controller's records have changed: + didChange: { [unowned self] _ in + self.tableView.endUpdates() + }) +``` + +See [GRDBDemoiOS](DemoApps/GRDBDemoiOS/GRDBDemoiOS) for an sample app that uses FetchedRecordsController. + +> :point_up: **Note**: our sample code above uses `unowned` references to the table view controller. This is a safe pattern as long as the table view controller owns the fetched records controller, and is deallocated from the main thread (this is usually the case). In other situations, prefer weak references. + + +### FetchedRecordsController Concurrency + +**A fetched records controller *can not* be used from any thread.** + +When the database itself can be read and modified from [any thread](#database-connections), fetched records controllers **must** be used from the main thread. Record changes are also [notified](#the-changes-notifications) on the main thread. + +**Change callbacks are invoked asynchronously.** This means that changes made from the main thread are *not* immediately notified. When you need to take immediate action, force the controller to refresh immediately with its `performFetch` method. In this case, changes callbacks are *not* called: + +```swift +// Change database on the main thread: +try dbQueue.inDatabase { db in + try Player(...).insert(db) +} +// Here callbacks have not been called yet. +// You can cancel them, and refresh records immediately: +try controller.performFetch() +``` + +> :point_up: **Note**: when the main thread does not fit your needs, give a serial dispatch queue to the controller initializer: the controller must then be used from this queue, and record changes are notified on this queue as well. +> +> ```swift +> let queue = DispatchQueue() +> queue.async { +> let controller = try FetchedRecordsController(..., queue: queue) +> controller.trackChanges { /* in queue */ } +> try controller.performFetch() +> } +> ``` + + +## Encryption + +**GRDB can encrypt your database with [SQLCipher](http://sqlcipher.net) v3.4.1.** + +You can use [CocoaPods](http://cocoapods.org/) (version 1.2 or higher), and specify in your `Podfile`: + +```ruby +use_frameworks! +pod 'GRDBCipher' +``` + +Alternatively, perform a manual installation of GRDB and SQLCipher: + +1. Clone the GRDB git repository, checkout the latest tagged version, and download SQLCipher sources: + + ```sh + cd [GRDB directory] + git checkout v2.0 + git submodule update --init SQLCipher/src + ``` + +2. Embed the `GRDB.xcodeproj` project in your own project. + +3. Add the `GRDBCipherOSX` or `GRDBCipheriOS` target in the **Target Dependencies** section of the **Build Phases** tab of your application target. + +4. Add the `GRDBCipher.framework` from the targetted platform to the **Embedded Binaries** section of the **General** tab of your target. + + +**You create and open an encrypted database** by providing a passphrase to your [database connection](#database-connections): + +```swift +import GRDBCipher + +var configuration = Configuration() +configuration.passphrase = "secret" +let dbQueue = try DatabaseQueue(path: "...", configuration: configuration) +``` + +**You can change the passphrase** of an already encrypted database: + +```swift +try dbQueue.change(passphrase: "newSecret") +``` + +Providing a passphrase won't encrypt a clear-text database that already exists, though. SQLCipher can't do that, and you will get an error instead: `SQLite error 26: file is encrypted or is not a database`. + +**To encrypt an existing clear-text database**, create a new and empty encrypted database, and copy the content of the clear-text database in it. The technique to do that is [documented](https://discuss.zetetic.net/t/how-to-encrypt-a-plaintext-sqlite-database-to-use-sqlcipher-and-avoid-file-is-encrypted-or-is-not-a-database-errors/868/1) by SQLCipher. With GRDB, it gives: + +```swift +// The clear-text database +let clearDBQueue = try DatabaseQueue(path: "/path/to/clear.db") + +// The encrypted database, at some distinct location: +var configuration = Configuration() +configuration.passphrase = "secret" +let encryptedDBQueue = try DatabaseQueue(path: "/path/to/encrypted.db", configuration: config) + +try clearDBQueue.inDatabase { db in + try db.execute("ATTACH DATABASE ? AS encrypted KEY ?", arguments: [encryptedDBQueue.path, "secret"]) + try db.execute("SELECT sqlcipher_export('encrypted')") + try db.execute("DETACH DATABASE encrypted") +} + +// Now the copy is done, and the clear-text database can be deleted. +``` + + +## Backup + +**You can backup (copy) a database into another.** + +Backups can for example help you copying an in-memory database to and from a database file when you implement NSDocument subclasses. + +```swift +let source: DatabaseQueue = ... // or DatabasePool +let destination: DatabaseQueue = ... // or DatabasePool +try source.backup(to: destination) +``` + +The `backup` method blocks the current thread until the destination database contains the same contents as the source database. + +When the source is a [database pool](#database-pools), concurrent writes can happen during the backup. Those writes may, or may not, be reflected in the backup, but they won't trigger any error. + + +Good To Know +============ + +This chapter covers general topics that you should be aware of. + +- [Avoiding SQL Injection](#avoiding-sql-injection) +- [Error Handling](#error-handling) +- [Unicode](#unicode) +- [Memory Management](#memory-management) +- [Data Protection](#data-protection) +- [Concurrency](#concurrency) +- [Performance](#performance) + + +## Avoiding SQL Injection + +SQL injection is a technique that lets an attacker nuke your database. + +> ![XKCD: Exploits of a Mom](https://imgs.xkcd.com/comics/exploits_of_a_mom.png) +> +> https://xkcd.com/327/ + +Here is an example of code that is vulnerable to SQL injection: + +```swift +// BAD BAD BAD +let name = textField.text +try dbQueue.inDatabase { db in + try db.execute("UPDATE students SET name = '\(name)' WHERE id = \(id)") +} +``` + +If the user enters a funny string like `Robert'; DROP TABLE students; --`, SQLite will see the following SQL, and drop your database table instead of updating a name as intended: + +```sql +UPDATE students SET name = 'Robert'; +DROP TABLE students; +--' WHERE id = 1 +``` + +To avoid those problems, **never embed raw values in your SQL queries**. The only correct technique is to provide [arguments](http://groue.github.io/GRDB.swift/docs/2.0/Structs/StatementArguments.html) to your SQL queries: + +```swift +// Good +let name = textField.text +try dbQueue.inDatabase { db in + try db.execute( + "UPDATE students SET name = ? WHERE id = ?", + arguments: [name, id]) +} +``` + +See [Executing Updates](#executing-updates) for more information on statement arguments. + + +## Error Handling + +GRDB can throw [DatabaseError](#databaseerror), [PersistenceError](#persistenceerror), or crash your program with a [fatal error](#fatal-errors). + +Considering that a local database is not some JSON loaded from a remote server, GRDB focuses on **trusted databases**. Dealing with [untrusted databases](#how-to-deal-with-untrusted-inputs) requires extra care. + +- [DatabaseError](#databaseerror) +- [PersistenceError](#persistenceerror) +- [Fatal Errors](#fatal-errors) +- [How to Deal with Untrusted Inputs](#how-to-deal-with-untrusted-inputs) +- [Error Log](#error-log) + + +### DatabaseError + +**DatabaseError** are thrown on SQLite errors: + +```swift +do { + try db.execute( + "INSERT INTO pets (masterId, name) VALUES (?, ?)", + arguments: [1, "Bobby"]) +} catch let error as DatabaseError { + // The SQLite error code: 19 (SQLITE_CONSTRAINT) + error.resultCode + + // The extended error code: 787 (SQLITE_CONSTRAINT_FOREIGNKEY) + error.extendedResultCode + + // The eventual SQLite message: FOREIGN KEY constraint failed + error.message + + // The eventual erroneous SQL query + // "INSERT INTO pets (masterId, name) VALUES (?, ?)" + error.sql + + // Full error description: + // "SQLite error 787 with statement `INSERT INTO pets (masterId, name) + // VALUES (?, ?)` arguments [1, "Bobby"]: FOREIGN KEY constraint failed"" + error.description +} +``` + +**SQLite uses codes to distinguish between various errors:** + +```swift +do { + try ... +} catch let error as DatabaseError where error.extendedResultCode == .SQLITE_CONSTRAINT_FOREIGNKEY { + // foreign key constraint error +} catch let error as DatabaseError where error.resultCode == .SQLITE_CONSTRAINT { + // any other constraint error +} catch let error as DatabaseError { + // any other database error +} +``` + +In the example above, `error.extendedResultCode` is a precise [extended result code](https://www.sqlite.org/rescode.html#extended_result_code_list), and `error.resultCode` is a less precise [primary result code](https://www.sqlite.org/rescode.html#primary_result_code_list). Extended result codes are refinements of primary result codes, as `SQLITE_CONSTRAINT_FOREIGNKEY` is to `SQLITE_CONSTRAINT`, for example. See [SQLite result codes](https://www.sqlite.org/rescode.html) for more information. + +As a convenience, extended result codes match their primary result code in a switch statement: + +```swift +do { + try ... +} catch let error as DatabaseError { + switch error.extendedResultCode { + case ResultCode.SQLITE_CONSTRAINT_FOREIGNKEY: + // foreign key constraint error + case ResultCode.SQLITE_CONSTRAINT: + // any other constraint error + default: + // any other database error + } +} +``` + +> :warning: **Warning**: SQLite has progressively introduced extended result codes accross its versions. For example, `SQLITE_CONSTRAINT_FOREIGNKEY` wasn't introduced yet on iOS 8.1. The [SQLite release notes](http://www.sqlite.org/changes.html) are unfortunately not quite clear about that: write your handling of extended result codes with care. + + +### PersistenceError + +**PersistenceError** is thrown by the [Persistable](#persistable-protocol) protocol, in a single case: when the `update` method could not find any row to update: + +```swift +do { + try player.update(db) +} catch PersistenceError.recordNotFound { + // There was nothing to update +} +``` + + +### Fatal Errors + +**Fatal errors notify that the program, or the database, has to be changed.** + +They uncover programmer errors, false assumptions, and prevent misuses. Here are a few examples: + +- **The code asks for a non-optional value, when the database contains NULL:** + + ```swift + // fatal error: could not convert NULL to String. + let name: String = row["name"] + ``` + + Solution: fix the contents of the database, use [NOT NULL constraints](#create-tables), or load an optional: + + ```swift + let name: String? = row["name"] + ``` + +- **Conversion from database value to Swift type fails:** + + ```swift + // fatal error: could not convert "Mom’s birthday" to Date. + let date: Date = row["date"] + + // fatal error: could not convert "" to URL. + let url: URL = row["url"] + ``` + + Solution: fix the contents of the database, or use [DatabaseValue](#databasevalue) to handle all possible cases: + + ```swift + let dbValue: DatabaseValue = row["date"] + if dbValue.isNull { + // Handle NULL + if let date = Date.fromDatabaseValue(dbValue) { + // Handle valid date + } else { + // Handle invalid date + } + ``` + +- **The database can't guarantee that the code does what it says:** + + ```swift + // fatal error: table players has no unique index on column email + try Player.deleteOne(db, key: ["email": "arthur@example.com"]) + ``` + + Solution: add a unique index to the players.email column, or use the `deleteAll` method to make it clear that you may delete more than one row: + + ```swift + try Player.filter(Column("email") == "arthur@example.com").deleteAll(db) + ``` + +- **Database connections are not reentrant:** + + ```swift + // fatal error: Database methods are not reentrant. + dbQueue.inDatabase { db in + dbQueue.inDatabase { db in + ... + } + } + ``` + + Solution: avoid reentrancy, and instead pass a database connection along. + + +### How to Deal with Untrusted Inputs + +Let's consider the code below: + +```swift +let sql = "SELECT ..." + +// Some untrusted arguments for the query +let arguments: [String: Any] = ... +let rows = try Row.fetchCursor(db, sql, arguments: StatementArguments(arguments)) + +while let row = try rows.next() { + // Some untrusted database value: + let date: Date? = row[0] +} +``` + +It has two opportunities to throw fatal errors: + +- **Untrusted arguments**: The dictionary may contain values that do not conform to the [DatabaseValueConvertible protocol](#values), or may miss keys required by the statement. +- **Untrusted database content**: The row may contain a non-null value that can't be turned into a date. + +In such a situation, you can still avoid fatal errors by exposing and handling each failure point, one level down in the GRDB API: + +```swift +// Untrusted arguments +if let arguments = StatementArguments(arguments) { + let statement = try db.makeSelectStatement(sql) + try statement.validate(arguments: arguments) + statement.unsafeSetArguments(arguments) + + var cursor = try Row.fetchCursor(statement) + while let row = try iterator.next() { + // Untrusted database content + let dbValue: DatabaseValue = row[0] + if dbValue.isNull { + // Handle NULL + if let date = Date.fromDatabaseValue(dbValue) { + // Handle valid date + } else { + // Handle invalid date + } + } +} +``` + +See [prepared statements](#prepared-statements) and [DatabaseValue](#databasevalue) for more information. + + +### Error Log + +**SQLite can be configured to invoke a callback function containing an error code and a terse error message whenever anomalies occur.** + +It is recommended that you setup, early in the lifetime of your application, the error logging callback: + +```swift +Database.logError = { (resultCode, message) in + NSLog("%@", "SQLite error \(resultCode): \(message)") +} +``` + +See [The Error And Warning Log](https://sqlite.org/errlog.html) for more information. + + +## Unicode + +SQLite lets you store unicode strings in the database. + +However, SQLite does not provide any unicode-aware string transformations or comparisons. + + +### Unicode functions + +The `UPPER` and `LOWER` built-in SQLite functions are not unicode-aware: + +```swift +// "JéRôME" +try String.fetchOne(db, "SELECT UPPER('Jérôme')") +``` + +GRDB extends SQLite with [SQL functions](#custom-sql-functions-and-aggregates) that call the Swift built-in string functions `capitalized`, `lowercased`, `uppercased`, `localizedCapitalized`, `localizedLowercased` and `localizedUppercased`: + +```swift +// "JÉRÔME" +let uppercase = DatabaseFunction.uppercase +try String.fetchOne(db, "SELECT \(uppercased.name)('Jérôme')") +``` + +Those unicode-aware string functions are also readily available in the [query interface](#sql-functions): + +``` +Player.select(nameColumn.uppercased) +``` + + +### String Comparison + +SQLite compares strings in many occasions: when you sort rows according to a string column, or when you use a comparison operator such as `=` and `<=`. + +The comparison result comes from a *collating function*, or *collation*. SQLite comes with three built-in collations that do not support Unicode: [binary, nocase, and rtrim](https://www.sqlite.org/datatype3.html#collation). + +GRDB comes with five extra collations that leverage unicode-aware comparisons based on the standard Swift String comparison functions and operators: + +- `unicodeCompare` (uses the built-in `<=` and `==` Swift operators) +- `caseInsensitiveCompare` +- `localizedCaseInsensitiveCompare` +- `localizedCompare` +- `localizedStandardCompare` + +A collation can be applied to a table column. All comparisons involving this column will then automatically trigger the comparison function: + +```swift +try db.create(table: "players") { t in + // Guarantees case-insensitive email unicity + t.column("email", .text).unique().collate(.nocase) + + // Sort names in a localized case insensitive way + t.column("name", .text).collate(.localizedCaseInsensitiveCompare) +} + +// Players are sorted in a localized case insensitive way: +let players = try Player.order(nameColumn).fetchAll(db) +``` + +> :warning: **Warning**: SQLite *requires* host applications to provide the definition of any collation other than binary, nocase and rtrim. When a database file has to be shared or migrated to another SQLite library of platform (such as the Android version of your application), make sure you provide a compatible collation. + +If you can't or don't want to define the comparison behavior of a column (see warning above), you can still use an explicit collation in SQL requests and in the [query interface](#the-query-interface): + +```swift +let collation = DatabaseCollation.localizedCaseInsensitiveCompare +let players = try Player.fetchAll(db, + "SELECT * FROM players ORDER BY name COLLATE \(collation.name))") +let players = try Player.order(nameColumn.collating(collation)).fetchAll(db) +``` + + +**You can also define your own collations**: + +```swift +let collation = DatabaseCollation("customCollation") { (lhs, rhs) -> NSComparisonResult in + // return the comparison of lhs and rhs strings. +} +dbQueue.add(collation: collation) // Or dbPool.add(collation: ...) +``` + + +## Memory Management + +Both SQLite and GRDB use non-essential memory that help them perform better. + +You can reclaim this memory with the `releaseMemory` method: + +```swift +// Release as much memory as possible. +dbQueue.releaseMemory() +dbPool.releaseMemory() +``` + +This method blocks the current thread until all current database accesses are completed, and the memory collected. + + +### Memory Management on iOS + +**The iOS operating system likes applications that do not consume much memory.** + +[Database queues](#database-queues) and [pools](#database-pools) can call the `releaseMemory` method for you, when application receives memory warnings, and when application enters background: call the `setupMemoryManagement` method after creating the queue or pool instance: + +``` +let dbQueue = try DatabaseQueue(...) +dbQueue.setupMemoryManagement(in: UIApplication.sharedApplication()) +``` + + +## Data Protection + +[Data Protection](https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/StrategiesforImplementingYourApp/StrategiesforImplementingYourApp.html#//apple_ref/doc/uid/TP40007072-CH5-SW21) lets you protect files so that they are encrypted and unavailable until the device is unlocked. + +Data protection can be enabled [globally](https://developer.apple.com/library/content/documentation/IDEs/Conceptual/AppDistributionGuide/AddingCapabilities/AddingCapabilities.html#//apple_ref/doc/uid/TP40012582-CH26-SW30) for all files created by an application. + +You can also explicitly protect a database, by configuring its enclosing *directory*. This will not only protect the database file, but also all [temporary files](https://www.sqlite.org/tempfiles.html) created by SQLite (including the persistent `.shm` and `.wal` files created by [database pools](#database-pools)). + +For example, to explicitely use [complete](https://developer.apple.com/reference/foundation/fileprotectiontype/1616200-complete) protection: + +```swift +// Paths +let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] +let directoryPath = (documentsPath as NSString).appendingPathComponent("database") +let databasePath = (directoryPath as NSString).appendingPathComponent("db.sqlite") + +// Create directory if needed +let fm = FileManager.default +var isDirectory: ObjCBool = false +if !fm.fileExists(atPath: directoryPath, isDirectory: &isDirectory) { + try fm.createDirectory(atPath: directoryPath, withIntermediateDirectories: false) +} else if !isDirectory.boolValue { + throw NSError(domain: NSCocoaErrorDomain, code: NSFileWriteFileExistsError, userInfo: nil) +} + +// Enable data protection +try fm.setAttributes([.protectionKey : FileProtectionType.complete], ofItemAtPath: directoryPath) + +// Open database +let dbQueue = try DatabaseQueue(path: databasePath) +``` + +When a database is protected, an application that runs in the background on a locked device won't be able to read or write from it. Instead, it will get [DatabaseError](#error-handling) with code [`SQLITE_IOERR`](https://www.sqlite.org/rescode.html#ioerr) (10) "disk I/O error", or [`SQLITE_AUTH`](https://www.sqlite.org/rescode.html#auth) (23) "not authorized". + +You can catch those errors and wait for [UIApplicationDelegate.applicationProtectedDataDidBecomeAvailable(_:)](https://developer.apple.com/reference/uikit/uiapplicationdelegate/1623044-applicationprotecteddatadidbecom) or [UIApplicationProtectedDataDidBecomeAvailable](https://developer.apple.com/reference/uikit/uiapplicationprotecteddatadidbecomeavailable) notification in order to retry the failed database operation. + + +## Concurrency + +- [Guarantees and Rules](#guarantees-and-rules) +- [Advanced DatabasePool](#advanced-databasepool) +- [DatabaseWriter and DatabaseReader Protocols](#databasewriter-and-databasereader-protocols) +- [Unsafe Concurrency APIs](#unsafe-concurrency-apis) +- [Dealing with External Connections](#dealing-with-external-connections) + + +### Guarantees and Rules + +GRDB ships with two concurrency modes: + +- [DatabaseQueue](#database-queues) opens a single database connection, and serializes all database accesses. +- [DatabasePool](#database-pools) manages a pool of several database connections, and allows concurrent reads and writes. + +**Both foster application safety**: regardless of the concurrency mode you choose, GRDB provides you with the same guarantees, as long as you follow three rules. + +- :bowtie: **Guarantee 1: writes are always serialized**. At every moment, there is no more than a single thread that is writing into the database. + +- :bowtie: **Guarantee 2: reads are always isolated**. This means that they are guaranteed an immutable view of the last committed state of the database, and that you can perform subsequent fetches without fearing eventual concurrent writes to mess with your application logic: + + ```swift + try dbPool.read { db in // or dbQueue.inDatabase { ... } + // Guaranteed to be equal + let count1 = try Player.fetchCount(db) + let count2 = try Player.fetchCount(db) + } + ``` + +- :bowtie: **Guarantee 3: requests don't fail**, unless a database constraint violation, a [programmer mistake](#error-handling), or a very low-level issue such as a disk error or an unreadable database file. GRDB grants *correct* use of SQLite, and particularly avoids locking errors and other SQLite misuses. + +Those guarantees hold as long as you follow three rules: + +- :point_up: **Rule 1**: Have a unique instance of DatabaseQueue or DatabasePool connected to any database file. + + This means that opening a new connection each time you access the database is probably a very bad idea. Do share a single connection instead. + + See, for example, [DemoApps/GRDBDemoiOS/Database.swift](DemoApps/GRDBDemoiOS/GRDBDemoiOS/Database.swift) for a sample code that properly sets up a single database queue that is available throughout the application. + + If there are several instances of database queues or pools that access the same database, a multi-threaded application will eventually face "database is locked" errors. See [Dealing with External Connections](#dealing-with-external-connections). + + ```swift + // SAFE CONCURRENCY + func currentUser(_ db: Database) throws -> User? { + return try User.fetchOne(db) + } + // dbQueue is a singleton defined somewhere in your app + let user = try dbQueue.inDatabase { db in // or dbPool.read { ... } + try currentUser(db) + } + + // UNSAFE CONCURRENCY + // This method fails when some other thread is currently writing into + // the database. + func currentUser() throws -> User? { + let dbQueue = try DatabaseQueue(...) + return try dbQueue.inDatabase { db in + try User.fetchOne(db) + } + } + let user = try currentUser() + ``` + +- :point_up: **Rule 2**: Group related statements within a single call to a DatabaseQueue or DatabasePool database access method. + + Those methods isolate your groups of related statements against eventual database updates performed by other threads, and guarantee a consistent view of the database. This isolation is only guaranteed *inside* the closure argument of those methods. Two consecutive calls *do not* guarantee isolation: + + ```swift + // SAFE CONCURRENCY + try dbPool.read { db in // or dbQueue.inDatabase { ... } + // Guaranteed to be equal: + let count1 = try Place.fetchCount(db) + let count2 = try Place.fetchCount(db) + } + + // UNSAFE CONCURRENCY + // Those two values may be different because some other thread may have + // modified the database between the two blocks: + let count1 = try dbPool.read { db in try Place.fetchCount(db) } + let count2 = try dbPool.read { db in try Place.fetchCount(db) } + ``` + + In the same vein, when you fetch values that depends on some database updates, group them: + + ```swift + // SAFE CONCURRENCY + try dbPool.write { db in + // The count is guaranteed to be non-zero + try Place(...).insert(db) + let count = try Place.fetchCount(db) + } + + // UNSAFE CONCURRENCY + // The count may be zero because some other thread may have performed + // a deletion between the two blocks: + try dbPool.write { db in try Place(...).insert(db) } + let count = try dbPool.read { db in try Place.fetchCount(db) } + ``` + + On that last example, see [Advanced DatabasePool](#advanced-databasepool) if you look after extra performance. + +- :point_up: **Rule 3**: When you perform several modifications of the database that temporarily put the database in an inconsistent state, group those modifications within a [transaction](#transactions-and-savepoints): + + ```swift + // SAFE CONCURRENCY + try dbPool.writeInTransaction { db in // or dbQueue.inTransaction { ... } + try Credit(destinationAccout, amount).insert(db) + try Debit(sourceAccount, amount).insert(db) + return .commit + } + + // UNSAFE CONCURRENCY + try dbPool.write { db in // or dbQueue.inDatabase { ... } + try Credit(destinationAccout, amount).insert(db) + try Debit(sourceAccount, amount).insert(db) + } + ``` + + Without transaction, `DatabasePool.read { ... }` may see the first statement, but not the second, and access a database where the balance of accounts is not zero. A highly bug-prone situation. + + So do use [transactions](#transactions-and-savepoints) in order to guarantee database consistency accross your application threads: that's what they are made for. + + +### Advanced DatabasePool + +[Database pools](#database-pools) are very concurrent, since all reads can run in parallel, and can even run during write operations. But writes are still serialized: at any given point in time, there is no more than a single thread that is writing into the database. + +When your application modifies the database, and then reads some value that depends on those modifications, you may want to avoid locking the writer queue longer than necessary: + +```swift +try dbPool.write { db in + // Increment the number of players + try Player(...).insert(db) + + // Read the number of players. The writer queue is still locked :-( + let count = try Player.fetchCount(db) +} +``` + +A wrong solution is to chain a write then a read, as below. Don't do that, because another thread may modify the database in between, and make the read unreliable: + +```swift +// WRONG +try dbPool.write { db in + // Increment the number of players + try Player(...).insert(db) +} +try dbPool.read { db in + // Read some random value :-( + let count = try Player.fetchCount(db) +} +``` + +The correct solution is the `readFromCurrentState` method, which must be called from within a write block: + +```swift +// CORRECT +try dbPool.write { db in + // Increment the number of players + try Player(...).insert(db) + + try dbPool.readFromCurrentState { db + // Read the number of players. The writer queue has been unlocked :-) + let count = try Player.fetchCount(db) + } +} +``` + +`readFromCurrentState` blocks until it can guarantee its closure argument an isolated access to the last committed state of the database. It then asynchronously executes the closure. If the isolated access can't be established, `readFromCurrentState` throws an error, and the closure is not executed. + +The closure can run concurrently with eventual updates performed after `readFromCurrentState`: those updates won't be visible from within the closure. In the example below, the number of players is guaranteed to be non-zero, even though it is fetched concurrently with the player deletion: + +```swift +try dbPool.write { db in + // Increment the number of players + try Player(...).insert(db) + + try dbPool.readFromCurrentState { db + // Guaranteed to be non-zero + let count = try Player.fetchCount(db) + } + + try Player.deleteAll(db) +} +``` + +[Transaction Observers](#transactionobserver-protocol) can also use `readFromCurrentState` in their `databaseDidCommit` method in order to process database changes without blocking other threads that want to write into the database. + + +### DatabaseWriter and DatabaseReader Protocols + +Both DatabaseQueue and DatabasePool adopt the [DatabaseReader](http://groue.github.io/GRDB.swift/docs/2.0/Protocols/DatabaseReader.html) and [DatabaseWriter](http://groue.github.io/GRDB.swift/docs/2.0/Protocols/DatabaseWriter.html) protocols. + +These protocols provide a unified API that lets you write safe concurrent code that targets both classes. + +However, database queues are not database pools, and DatabaseReader and DatabaseWriter provide the *smallest* common guarantees. They require more discipline: + +- Pools are less forgiving than queues when one overlooks a transaction (see [concurrency rule 3](#guarantees-and-rules)). +- DatabaseWriter.readFromCurrentState is synchronous, or asynchronous, depending on whether it is run by a queue or a pool (see [advanced DatabasePool](#advanced-databasepool)). It thus requires higher libDispatch skills, and more complex synchronization code. +- The definition of "current state" in DatabaseWriter.readFromCurrentState is [delicate](http://groue.github.io/GRDB.swift/docs/2.0/Protocols/DatabaseWriter.html#/s:FP4GRDB14DatabaseWriter20readFromCurrentStateFzFCS_8DatabaseT_T_). + +DatabaseReader and DatabaseWriter are not a tool for applications that hesitate between DatabaseQueue and DatabasePool, and look for a common API. As seen above, the protocols actually make applications harder to write correctly. Instead, they target generic code that has *both* queues and pools in mind. The built-in AnyDatabaseReader and AnyDatabaseWriter type erasers serve the same purpose. + +DatabaseWriter and DatabaseReader fuel, for example: + +- [Migrations](#migrations) +- [FetchedRecordsController](#fetchedrecordscontroller) +- [RxGRDB](http://github.com/RxSwiftCommunity/RxGRDB) + + +### Unsafe Concurrency APIs + +**Database queues, pools, as well as their common protocols `DatabaseReader` and `DatabaseWriter` provide *unsafe* APIs.** Unsafe APIs lift [concurrency guarantees](#guarantees-and-rules), and allow advanced yet unsafe patterns. + +- **`unsafeRead`** + + The `unsafeRead` method is synchronous, and blocks the current thread until your database statements are executed in a protected dispatch queue. GRDB does just the bare minimum to provide a database connection that can read. + + When used on a database pool, reads are no longer isolated: + + ```swift + dbPool.unsafeRead { db in + // Those two values may be different because some other thread + // may have inserted or deleted a player between the two requests: + let count1 = try Player.fetchCount(db) + let count2 = try Player.fetchCount(db) + } + ``` + + When used on a datase queue, the closure argument is allowed to write in the database. + +- **`unsafeReentrantRead`** + + The `unsafeReentrantRead` behaves just as `unsafeRead` (see above), and allows reentrant calls: + + ```swift + dbPool.read { db1 in + // No "Database methods are not reentrant" fatal error: + dbPool.unsafeReentrantRead { db2 in + dbPool.unsafeReentrantRead { db3 in + ... + } + } + } + ``` + + Reentrant database accesses make it very easy to break the second [safety rule](#guarantees-and-rules), which says: "group related statements within a single call to a DatabaseQueue or DatabasePool database access method.". Using a reentrant method is pretty much likely the sign of a wrong application architecture that needs refactoring. + + Reentrant methods have been introduced in order to support [RxGRDB](http://github.com/RxSwiftCommunity/RxGRDB), a set of reactive extensions to GRDB based on [RxSwift](https://github.com/ReactiveX/RxSwift) that need precise scheduling. + +- **`unsafeReentrantWrite`** + + The `unsafeReentrantWrite` method is synchronous, and blocks the current thread until your database statements are executed in a protected dispatch queue. Writes are serialized: eventual concurrent database updates are postponed until the block has executed. + + Reentrant calls are allowed: + + ```swift + dbQueue.inDatabase { db1 in + // No "Database methods are not reentrant" fatal error: + dbQueue.unsafeReentrantWrite { db2 in + dbQueue.unsafeReentrantWrite { db3 in + ... + } + } + } + ``` + + Reentrant database accesses make it very easy to break the second [safety rule](#guarantees-and-rules), which says: "group related statements within a single call to a DatabaseQueue or DatabasePool database access method.". Using a reentrant method is pretty much likely the sign of a wrong application architecture that needs refactoring. + + Reentrant methods have been introduced in order to support [RxGRDB](http://github.com/RxSwiftCommunity/RxGRDB), a set of reactive extensions to GRDB based on [RxSwift](https://github.com/ReactiveX/RxSwift) that need precise scheduling. + + +### Dealing with External Connections + +The first rule of GRDB is: + +- **[Rule 1](#guarantees-and-rules)**: Have a unique instance of DatabaseQueue or DatabasePool connected to any database file. + +This means that dealing with external connections is not a focus of GRDB. [Guarantees](#guarantees-and-rules) of GRDB may or may not hold as soon as some external connection modifies a database. + +If you absolutely need multiple connections, then: + +- Reconsider your position +- Read about [isolation in SQLite](https://www.sqlite.org/isolation.html) +- Learn about [locks and transactions](https://www.sqlite.org/lang_transaction.html) +- Become a master of the [WAL mode](https://www.sqlite.org/wal.html) +- Prepare to setup a [busy handler](https://www.sqlite.org/c3ref/busy_handler.html) with [Configuration.busyMode](http://groue.github.io/GRDB.swift/docs/2.0/Structs/Configuration.html) +- [Ask questions](https://github.com/groue/GRDB.swift/issues) + + +## Performance + +GRDB is a reasonably fast library, and can deliver quite efficient SQLite access. See [Comparing the Performances of Swift SQLite libraries](https://github.com/groue/GRDB.swift/wiki/Performance) for an overview. + +You'll find below general advice when you do look after performance: + +- Focus +- Know your platform +- Use transactions +- Don't do useless work +- Learn about SQL strengths and weaknesses +- Avoid strings & dictionaries + + +### Performance tip: focus + +You don't know which part of your program needs improvement until you have run a benchmarking tool. + +Don't make any assumption, avoid optimizing code too early, and use [Instruments](https://developer.apple.com/library/ios/documentation/ToolsLanguages/Conceptual/Xcode_Overview/MeasuringPerformance.html). + + +### Performance tip: know your platform + +If your application processes a huge JSON file and inserts thousands of rows in the database right from the main thread, it will quite likely become unresponsive, and provide a sub-quality user experience. + +If not done yet, read the [Concurrency Programming Guide](https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008091) and learn how to perform heavy computations without blocking your application. + +Most GRBD APIs are [synchronous](#database-connections). Spawning them into parallel queues is as easy as: + +```swift +DispatchQueue.global().async { + dbQueue.inDatabase { db in + // Perform database work + } + DispatchQueue.main.async { + // update your user interface + } +} +``` + + +### Performance tip: use transactions + +Performing multiple updates to the database is much faster when executed inside a [transaction](#transactions-and-savepoints). This is because a transaction allows SQLite to postpone writing changes to disk until the final commit: + +```swift +// Inefficient +try dbQueue.inDatabase { db in + for player in players { + try player.insert(db) + } +} + +// Efficient +try dbQueue.inTransaction { db in + for player in players { + try player.insert(db) + } + return .Commit +} +``` + + +### Performance tip: don't do useless work + +Obviously, no code is faster than any code. + + +**Don't fetch columns you don't use** + +```swift +// SELECT * FROM players +try Player.fetchAll(db) + +// SELECT id, name FROM players +try Player.select(idColumn, nameColumn).fetchAll(db) +``` + +If your Player type can't be built without other columns (it has non-optional properties for other columns), *do define and use a different type*. + + +**Don't fetch rows you don't use** + +Use [fetchOne](#fetching-methods) when you need a single value, and otherwise limit your queries at the database level: + +```swift +// Wrong way: this code may discard hundreds of useless database rows +let players = try Player.order(scoreColumn.desc).fetchAll(db) +let hallOfFame = players.prefix(5) + +// Better way +let hallOfFame = try Player.order(scoreColumn.desc).limit(5).fetchAll(db) +``` + + +**Don't copy values unless necessary** + +Particularly: the Array returned by the `fetchAll` method, and the cursor returned by `fetchCursor` aren't the same: + +`fetchAll` copies all values from the database into memory, when `fetchCursor` iterates database results as they are generated by SQLite, taking profit from SQLite efficiency. + +You should only load arrays if you need to keep them for later use (such as iterating their contents in the main thread). Otherwise, use `fetchCursor`. + +See [fetching methods](#fetching-methods) for more information about `fetchAll` and `fetchCursor`. See also the [Row.dataNoCopy](#data-and-memory-savings) method. + + +**Don't update rows unless necessary** + +An UPDATE statement is costly: SQLite has to look for the updated row, update values, and write changes to disk. + +When the overwritten values are the same as the existing ones, it's thus better to avoid performing the UPDATE statement. + +The [Record](#record-class) class can help you: it provides [changes tracking](#changes-tracking): + +```swift +if player.hasPersistentChangedValues { + try player.update(db) +} +``` + + +### Performance tip: learn about SQL strengths and weaknesses + +Consider a simple use case: your store application has to display a list of authors with the number of available books: + +- J. M. Coetzee (6) +- Herman Melville (1) +- Alice Munro (3) +- Kim Stanley Robinson (7) +- Oliver Sacks (4) + +The following code is inefficient. It is an example of the [N+1 problem](http://stackoverflow.com/questions/97197/what-is-the-n1-selects-issue), because it performs one query to load the authors, and then N queries, as many as there are authors. This turns very inefficient as the number of authors grows: + +```swift +// SELECT * FROM authors +let authors = try Author.fetchAll(db) +for author in authors { + // SELECT COUNT(*) FROM books WHERE authorId = ... + author.bookCount = try Book.filter(authorIdColumn == author.id).fetchCount(db) +} +``` + +Instead, perform *a single query*: + +```swift +let sql = """ + SELECT authors.*, COUNT(books.id) AS bookCount + FROM authors + LEFT JOIN books ON books.authorId = authors.id + GROUP BY authors.id + """ +let authors = try Author.fetchAll(db, sql) +``` + +In the example above, consider extending your Author with an extra bookCount property, or define and use a different type. + +Generally, define indexes on your database tables, and use SQLite's efficient query planning: + +- [Query Planning](https://www.sqlite.org/queryplanner.html) +- [CREATE INDEX](https://www.sqlite.org/lang_createindex.html) +- [The SQLite Query Planner](https://www.sqlite.org/optoverview.html) +- [EXPLAIN QUERY PLAN](https://www.sqlite.org/eqp.html) + + +### Performance tip: avoid strings & dictionaries + +The String and Dictionary Swift types are better avoided when you look for the best performance. + +Now GRDB [records](#records), for your convenience, do use strings and dictionaries: + +```swift +class Player : Record { + var id: Int64? + var name: String + var email: String + + required init(_ row: Row) { + id = row["id"] // String + name = row["name"] // String + email = row["email"] // String + super.init() + } + + override func encode(to container: inout PersistenceContainer) { + container["id"] = id // String + container["name"] = name // String + container["email"] = email // String + } +} +``` + +When convenience hurts performance, you can still use records, but you have better avoiding their string and dictionary-based methods. + +For example, when fetching values, prefer loading columns by index: + +```swift +// Strings & dictionaries +let players = try Player.fetchAll(db) + +// Column indexes +// SELECT id, name, email FROM players +let request = Player.select(idColumn, nameColumn, emailColumn) +let rows = try Row.fetchCursor(db, request) +while let row = try rows.next() { + let id: Int64 = row[0] + let name: String = row[1] + let email: String = row[2] + let player = Player(id: id, name: name, email: email) + ... +} +``` + +When inserting values, use reusable [prepared statements](#prepared-statements), and set statements values with an *array*: + +```swift +// Strings & dictionaries +for player in players { + try player.insert(db) +} + +// Prepared statement +let insertStatement = db.prepareStatement("INSERT INTO players (name, email) VALUES (?, ?)") +for player in players { + // Only use the unsafe arguments setter if you are sure that you provide + // all statement arguments. A mistake can store unexpected values in + // the database. + insertStatement.unsafeSetArguments([player.name, player.email]) + try insertStatement.execute() +} +``` + + +FAQ +=== + +- [How do I close a database connection?](#how-do-i-close-a-database-connection) +- [How do I open a database stored as a resource of my application?](#how-do-i-open-a-database-stored-as-a-resource-of-my-application) +- [Generic parameter 'T' could not be inferred](#generic-parameter-t-could-not-be-inferred) +- [Compilation takes a long time](#compilation-takes-a-long-time) +- [SQLite error 10 "disk I/O error", SQLite error 23 "not authorized"](#sqlite-error-10-disk-io-error-sqlite-error-23-not-authorized) +- [What Are Experimental Features?](#what-are-experimental-features) + + +### How do I close a database connection? + +Database connections are managed by [database queues](#database-queues) and [pools](#database-pools). A connection is closed when its database queue or pool is deallocated, and all usages of this connection are completed. + +Database accesses that run in background threads postpone the closing of connections. + + +### How do I open a database stored as a resource of my application? + +If your application does not need to modify the database, open a read-only [connection](#database-connections) to your resource: + +```swift +var configuration = Configuration() +configuration.readonly = true +let dbPath = Bundle.main.path(forResource: "db", ofType: "sqlite")! +let dbQueue = try DatabaseQueue(path: dbPath, configuration: configuration) +``` + +If the application should modify the database, you need to copy it to a place where it can be modified. For example, in the Documents folder. Only then, open a [connection](#database-connections): + +```swift +let fm = FileManager.default +let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] +let dbPath = (documentsPath as NSString).appendingPathComponent("db.sqlite") +if !fm.fileExists(atPath: dbPath) { + let dbResourcePath = Bundle.main.path(forResource: "db", ofType: "sqlite")! + try fm.copyItem(atPath: dbResourcePath, toPath: dbPath) +} +let dbQueue = try DatabaseQueue(path: dbPath) +``` + + +### Generic parameter 'T' could not be inferred + +You may get this error when using DatabaseQueue.inDatabase, DatabasePool.read, or DatabasePool.write: + +```swift +// Generic parameter 'T' could not be inferred +let x = try dbQueue.inDatabase { db in + let result = try String.fetchOne(db, ...) + return result +} +``` + +This is a Swift compiler issue (see [SR-1570](https://bugs.swift.org/browse/SR-1570)). + +The general workaround is to explicitly declare the type of the closure result: + +```swift +// General Workaround +let string = try dbQueue.inDatabase { db -> String? in + let result = try String.fetchOne(db, ...) + return result +} +``` + +You can also, when possible, write a single-line closure: + +```swift +// Single-line closure workaround: +let string = try dbQueue.inDatabase { db in + try String.fetchOne(db, ...) +} +``` + + +### SQLite error 10 "disk I/O error", SQLite error 23 "not authorized" + +Those errors may be the sign that SQLite can't access the database due to [data protection](#data-protection). + +When your application should be able to run in the background on a locked device, it has to catch this error, and, for example, wait for [UIApplicationDelegate.applicationProtectedDataDidBecomeAvailable(_:)](https://developer.apple.com/reference/uikit/uiapplicationdelegate/1623044-applicationprotecteddatadidbecom) or [UIApplicationProtectedDataDidBecomeAvailable](https://developer.apple.com/reference/uikit/uiapplicationprotecteddatadidbecomeavailable) notification and retry the failed database operation. + +This error can also be prevented altogether by using a more relaxed [file protection](https://developer.apple.com/reference/foundation/filemanager/1653059-file_protection_values). + + +### What Are Experimental Features? + +Since GRDB 1.0, all backwards compatibility guarantees of [semantic versioning](http://semver.org) apply: no breaking change will happen until the next major version of the library. + +There is an exception, though: *experimental features*, marked with the "**:fire: EXPERIMENTAL**" badge. Those are advanced features that are too young, or lack user feedback. They are not stabilized yet. + +Those experimental features are not protected by semantic versioning, and may break between two minor releases of the library. To help them becoming stable, [your feedback](https://github.com/groue/GRDB.swift/issues) is greatly appreciated. + + +Sample Code +=========== + +- The [Documentation](#documentation) is full of GRDB snippets. +- [GRDBDemoiOS](DemoApps/GRDBDemoiOS/GRDBDemoiOS): A sample iOS application. +- [WWDC Companion](https://github.com/groue/WWDCCompanion): A sample iOS application. +- Check `GRDB.xcworkspace`: it contains GRDB-enabled playgrounds to play with. +- How to synchronize a database table with a JSON payload: [JSONSynchronization.playground](Playgrounds/JSONSynchronization.playground/Contents.swift) + + +--- + +**Thanks** + +- [Pierlis](http://pierlis.com), where we write great software. +- [Vladimir Babin](https://github.com/Chiliec), [Pascal Edmond](https://github.com/pakko972), [Andrey Fidrya](https://github.com/zmeyc), [Cristian Filipov](https://github.com/cfilipov), [David Hart](https://github.com/hartbit), [Brad Lindsay](https://github.com/bfad), [@peter-ss](https://github.com/peter-ss), [Pierre-Loïc Raynaud](https://github.com/pierlo), [Stefano Rodriguez](https://github.com/sroddy) [Steven Schveighoffer](https://github.com/schveiguy), [@swiftlyfalling](https://github.com/swiftlyfalling), and [Kevin Wooten](https://github.com/kdubb) for their contributions, help, and feedback on GRDB. +- [@aymerick](https://github.com/aymerick) and [Mathieu "Kali" Poumeyrol](https://github.com/kali) because SQL. +- [ccgus/fmdb](https://github.com/ccgus/fmdb) for its excellency. diff --git a/Pods/GRDB.swift/Support/GRDB-Bridging.h b/Pods/GRDB.swift/Support/GRDB-Bridging.h new file mode 100644 index 0000000..e69de29 diff --git a/Pods/GRDB.swift/Support/GRDB.h b/Pods/GRDB.swift/Support/GRDB.h new file mode 100644 index 0000000..69ad6ed --- /dev/null +++ b/Pods/GRDB.swift/Support/GRDB.h @@ -0,0 +1,10 @@ +@import Foundation; + +//! Project version number for GRDB. +FOUNDATION_EXPORT double GRDB_VersionNumber; + +//! Project version string for GRDB. +FOUNDATION_EXPORT const unsigned char GRDB_VersionString[]; + +#import <GRDB/GRDB-Bridging.h> + diff --git a/Pods/GRDB.swift/Support/grdb_config.h b/Pods/GRDB.swift/Support/grdb_config.h new file mode 100644 index 0000000..63d3d1d --- /dev/null +++ b/Pods/GRDB.swift/Support/grdb_config.h @@ -0,0 +1,28 @@ +#ifndef grdb_config_h +#define grdb_config_h + +#if defined(COCOAPODS) + #if defined(GRDBCIPHER) + #include <SQLCipher/sqlite3.h> + #else + #include <sqlite3.h> + #endif +#else + #if defined(GRDBCIPHER) + #include <GRDBCipher/sqlite3.h> + #elsif defined(GRDBCUSTOMSQLITE) + #include <GRDBCustom/sqlite3.h> + #else + #include <sqlite3.h> + #endif +#endif + +typedef void(*errorLogCallback)(void *pArg, int iErrCode, const char *zMsg); + +// Wrapper around sqlite3_config(SQLITE_CONFIG_LOG, ...) which is a variadic +// function that can't be used from Swift. +static inline void registerErrorLogCallback(errorLogCallback callback) { + sqlite3_config(SQLITE_CONFIG_LOG, callback, 0); +} + +#endif /* grdb_config_h */ diff --git a/Pods/GRDB.swift/Support/module.modulemap b/Pods/GRDB.swift/Support/module.modulemap new file mode 100644 index 0000000..04ac555 --- /dev/null +++ b/Pods/GRDB.swift/Support/module.modulemap @@ -0,0 +1,8 @@ +framework module GRDB { + umbrella header "GRDB.h" + + export * + module * { export * } + + header "grdb_config.h" +} diff --git a/Pods/Local Podspecs/FeedKit.podspec.json b/Pods/Local Podspecs/FeedKit.podspec.json new file mode 100644 index 0000000..b54ec48 --- /dev/null +++ b/Pods/Local Podspecs/FeedKit.podspec.json @@ -0,0 +1,21 @@ +{ + "name": "FeedKit", + "version": "7.0.1", + "license": "MIT", + "summary": "An RSS, Atom and JSON Feed parser written in Swift", + "homepage": "https://github.com/nmdias/FeedKit", + "authors": { + "Nuno Manuel Dias": "nmdias.pt@gmail.com" + }, + "source": { + "git": "https://github.com/nmdias/FeedKit.git", + "tag": "7.0.1" + }, + "platforms": { + "ios": "8.0", + "osx": "10.10", + "tvos": "9.0", + "watchos": "2.0" + }, + "source_files": "Sources/**/*.swift" +} diff --git a/Pods/MASPreferences/Framework/MASPreferences.h b/Pods/MASPreferences/Framework/MASPreferences.h new file mode 100644 index 0000000..1130c65 --- /dev/null +++ b/Pods/MASPreferences/Framework/MASPreferences.h @@ -0,0 +1,2 @@ +#import "MASPreferencesViewController.h" +#import "MASPreferencesWindowController.h" diff --git a/Pods/MASPreferences/Framework/MASPreferencesViewController.h b/Pods/MASPreferences/Framework/MASPreferencesViewController.h new file mode 100644 index 0000000..7704923 --- /dev/null +++ b/Pods/MASPreferences/Framework/MASPreferencesViewController.h @@ -0,0 +1,66 @@ +// +// Any controller providing preference pane view must support this protocol +// + +#import <AppKit/AppKit.h> + +NS_ASSUME_NONNULL_BEGIN + +#if !defined(NSUserInterfaceItemIdentifier) +typedef NSString * NSUserInterfaceItemIdentifier NS_EXTENSIBLE_STRING_ENUM; +#endif + +/*! + * Requirements for the Preferences panel + */ +@protocol MASPreferencesViewController <NSObject> + +/*! + * Unique identifier of the Panel represented by the view controller. + */ +@property (nonatomic, readonly, nullable) NSUserInterfaceItemIdentifier identifier; + +/*! + * Toolbar icon for the Panel represented by the view controller. + */ +@property (nonatomic, readonly, nullable) NSImage *toolbarItemImage; + +/*! + * Toolbar item label for the Panel represented by the view controller. + * + * This label may be used as a Preferences window title. + */ +@property (nonatomic, readonly, nullable) NSString *toolbarItemLabel; + +@optional + +/*! + * Called when selection goes to the Panel represented by the view controller. + */ +- (void)viewWillAppear; + +/*! + * Called when selection goes to another Panel. + */ +- (void)viewDidDisappear; + +/*! + * Returns initial control in the key view loop. + * + * @return The view to focus on automatically when the panel is open. + */ +- (__kindof NSView *)initialKeyView; + +/*! + * The flag used to detect if the Prerences window can be resized horizontally. + */ +@property (nonatomic, readonly) BOOL hasResizableWidth; + +/*! + * The flag used to detect if the Prerences window can be resized vertically. + */ +@property (nonatomic, readonly) BOOL hasResizableHeight; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Pods/MASPreferences/Framework/MASPreferencesWindowController.h b/Pods/MASPreferences/Framework/MASPreferencesWindowController.h new file mode 100644 index 0000000..e5ea47a --- /dev/null +++ b/Pods/MASPreferences/Framework/MASPreferencesWindowController.h @@ -0,0 +1,124 @@ +// +// You create an application Preferences window using code like this: +// _preferencesWindowController = [[MASPreferencesWindowController alloc] initWithViewControllers:controllers title:title] +// +// To open the Preferences window: +// [_preferencesWindowController showWindow:sender] +// + +#import <AppKit/AppKit.h> + +@protocol MASPreferencesViewController; + +NS_ASSUME_NONNULL_BEGIN + +/*! + * Notification posted when you switch selected panel in Preferences. + */ +extern NSString * const kMASPreferencesWindowControllerDidChangeViewNotification; + +/*! + * Window controller for managing Preference view controllers. + */ +__attribute__((__visibility__("default"))) +#if MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5 +@interface MASPreferencesWindowController : NSWindowController <NSToolbarDelegate, NSWindowDelegate> +#else +@interface MASPreferencesWindowController : NSWindowController +#endif +{ +@private + NSMutableArray *_viewControllers; + NSMutableDictionary *_minimumViewRects; + NSString *_title; + NSViewController <MASPreferencesViewController> *_selectedViewController; + NSToolbar * __unsafe_unretained _toolbar; +} + +/*! + * Child view controllers in the Preferences window. + */ +@property (nonatomic, readonly) NSMutableArray *viewControllers; + +/*! + * Index of selected panel in the Preferences window. + */ +@property (nonatomic, readonly) NSUInteger indexOfSelectedController; + +/*! + * View controller representing selected panel in the Preferences window. + */ +@property (nonatomic, readonly) NSViewController <MASPreferencesViewController> *selectedViewController; + +/*! + * Optional window title provided in the initializer. + */ +@property (nonatomic, copy, readonly, nullable) NSString *title; + +/*! + * The toolbar managed by the Preferences window. + */ +@property (nonatomic, unsafe_unretained) IBOutlet NSToolbar *toolbar; + +/*! + * Creates new a window controller for Preferences with custom title. + * + * @param viewControllers Non-empty list of view controllers representing Preference panels. + * @param title Optional title for the Preferneces window. Pass `nil` to show the title provided by selected view controller. + * + * @return A new controller with the given title. + */ +- (instancetype)initWithViewControllers:(NSArray *)viewControllers title:(NSString * _Nullable)title; +- (instancetype)init __attribute((unavailable("Please use initWithViewControllers:title:"))); + +/*! + * Creates new a window controller for Preferences with a flexible title. + * + * @param viewControllers Non-empty list of view controllers representing Preference panels. + * + * @return A new controller with title depending on selected view controller. + */ +- (instancetype)initWithViewControllers:(NSArray *)viewControllers; + +/*! + * Appends new panel to the Preferences window. + * + * @param viewController View controller representing new panel. + */ +- (void)addViewController:(NSViewController <MASPreferencesViewController> *)viewController; + +/*! + * Changes selection in the Preferences toolbar. + * + * @param controllerIndex Position of the new panel to select in the toolbar. + */ +- (void)selectControllerAtIndex:(NSUInteger)controllerIndex; + +/*! + * Changes selection in the Preferences toolbar using panel identifier. + * + * @param identifier String identifier of the view controller to select. + */ +- (void)selectControllerWithIdentifier:(NSString *)identifier; + +/*! + * Useful action for switching to the next panel. + * + * For example, you may connect it to the main menu. + * + * @param sender Menu or toolbar item. + */ +- (IBAction)goNextTab:(id _Nullable)sender; + +/*! + * Useful action for switching to the previous panel. + * + * For example, you may connect it to the main menu. + * + * @param sender Menu or toolbar item. + */ +- (IBAction)goPreviousTab:(id _Nullable)sender; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Pods/MASPreferences/Framework/MASPreferencesWindowController.m b/Pods/MASPreferences/Framework/MASPreferencesWindowController.m new file mode 100644 index 0000000..1106320 --- /dev/null +++ b/Pods/MASPreferences/Framework/MASPreferencesWindowController.m @@ -0,0 +1,336 @@ +#import "MASPreferencesWindowController.h" +#import "MASPreferencesViewController.h" + +NSString *const kMASPreferencesWindowControllerDidChangeViewNotification = @"MASPreferencesWindowControllerDidChangeViewNotification"; + +static NSString *const kMASPreferencesFrameTopLeftKey = @"MASPreferences Frame Top Left"; +static NSString *const kMASPreferencesSelectedViewKey = @"MASPreferences Selected Identifier View"; + +static NSString * PreferencesKeyForViewBounds (NSString *identifier) +{ + return [NSString stringWithFormat:@"MASPreferences %@ Frame", identifier]; +} + +@interface MASPreferencesWindowController () // Private + +- (NSViewController <MASPreferencesViewController> *)viewControllerForIdentifier:(NSString *)identifier; + +@property (readonly) NSArray *toolbarItemIdentifiers; +@property (nonatomic, retain) NSViewController <MASPreferencesViewController> *selectedViewController; + +@end + +#pragma mark - + +@implementation MASPreferencesWindowController + +@synthesize viewControllers = _viewControllers; +@synthesize selectedViewController = _selectedViewController; +@synthesize title = _title; +@synthesize toolbar = _toolbar; + +#pragma mark - + +- (instancetype)initWithViewControllers:(NSArray *)viewControllers +{ + return [self initWithViewControllers:viewControllers title:nil]; +} + +- (instancetype)initWithViewControllers:(NSArray *)viewControllers title:(NSString *)title +{ + NSParameterAssert(viewControllers.count > 0); + NSString *nibPath = [[NSBundle bundleForClass:MASPreferencesWindowController.class] pathForResource:@"MASPreferencesWindow" ofType:@"nib"]; + if ((self = [super initWithWindowNibPath:nibPath owner:self])) + { + _viewControllers = [viewControllers mutableCopy]; + _minimumViewRects = [[NSMutableDictionary alloc] init]; + _title = [title copy]; + } + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [[self window] setDelegate:nil]; + for (NSToolbarItem *item in [self.toolbar items]) { + item.target = nil; + item.action = nil; + } + self.toolbar.delegate = nil; +} + +- (void)addViewController:(NSViewController <MASPreferencesViewController> *)viewController +{ + NSParameterAssert(viewController); + [_viewControllers addObject: viewController]; + [_toolbar insertItemWithItemIdentifier: [viewController identifier] atIndex: ([_viewControllers count] - 1)]; + [_toolbar validateVisibleItems]; +} + +#pragma mark - + +- (void)windowDidLoad +{ + if ([self.title length] > 0) + [[self window] setTitle:self.title]; + + if ([self.viewControllers count]) + self.selectedViewController = [self viewControllerForIdentifier:[[NSUserDefaults standardUserDefaults] stringForKey:kMASPreferencesSelectedViewKey]] ?: [self firstViewController]; + + NSString *origin = [[NSUserDefaults standardUserDefaults] stringForKey:kMASPreferencesFrameTopLeftKey]; + if (origin) + [self.window setFrameTopLeftPoint:NSPointFromString(origin)]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowDidMove:) name:NSWindowDidMoveNotification object:self.window]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowDidResize:) name:NSWindowDidResizeNotification object:self.window]; +} + +- (NSViewController <MASPreferencesViewController> *)firstViewController { + for (id viewController in self.viewControllers) + if ([viewController isKindOfClass:[NSViewController class]]) + return viewController; + + return nil; +} + +#pragma mark - +#pragma mark NSWindowDelegate + +- (BOOL)windowShouldClose:(id __unused)sender +{ + return !self.selectedViewController || [self.selectedViewController commitEditing]; +} + +- (void)windowDidMove:(NSNotification* __unused)aNotification +{ + [[NSUserDefaults standardUserDefaults] setObject:NSStringFromPoint(NSMakePoint(NSMinX([self.window frame]), NSMaxY([self.window frame]))) forKey:kMASPreferencesFrameTopLeftKey]; +} + +- (void)windowDidResize:(NSNotification* __unused)aNotification +{ + NSViewController <MASPreferencesViewController> *viewController = self.selectedViewController; + if (viewController) + [[NSUserDefaults standardUserDefaults] setObject:NSStringFromRect([viewController.view bounds]) forKey:PreferencesKeyForViewBounds(viewController.identifier)]; +} + +#pragma mark - +#pragma mark Accessors + +- (NSArray *)toolbarItemIdentifiers +{ + NSMutableArray *identifiers = [NSMutableArray arrayWithCapacity:_viewControllers.count]; + for (id viewController in _viewControllers) + if (viewController == [NSNull null]) + [identifiers addObject:NSToolbarFlexibleSpaceItemIdentifier]; + else + [identifiers addObject:[viewController identifier]]; + return identifiers; +} + +#pragma mark - + +- (NSUInteger)indexOfSelectedController +{ + NSUInteger index = [self.toolbarItemIdentifiers indexOfObject:self.selectedViewController.identifier]; + return index; +} + +#pragma mark - +#pragma mark NSToolbarDelegate + +- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar * __unused)toolbar +{ + NSArray *identifiers = self.toolbarItemIdentifiers; + return identifiers; +} + +- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar * __unused)toolbar +{ + NSArray *identifiers = self.toolbarItemIdentifiers; + return identifiers; +} + +- (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar * __unused)toolbar +{ + NSArray *identifiers = self.toolbarItemIdentifiers; + return identifiers; +} + +- (NSToolbarItem *)toolbar:(NSToolbar * __unused)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL __unused)flag +{ + NSToolbarItem *toolbarItem = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier]; + NSArray *identifiers = self.toolbarItemIdentifiers; + NSUInteger controllerIndex = [identifiers indexOfObject:itemIdentifier]; + if (controllerIndex != NSNotFound) + { + id <MASPreferencesViewController> controller = [_viewControllers objectAtIndex:controllerIndex]; + toolbarItem.image = controller.toolbarItemImage; + toolbarItem.label = controller.toolbarItemLabel; + toolbarItem.target = self; + toolbarItem.action = @selector(toolbarItemDidClick:); + } + return toolbarItem; +} + +#pragma mark - +#pragma mark Private methods + +- (NSViewController <MASPreferencesViewController> *)viewControllerForIdentifier:(NSString *)identifier +{ + for (id viewController in self.viewControllers) { + if (viewController == [NSNull null]) continue; + if ([[viewController identifier] isEqualToString:identifier]) + return viewController; + } + return nil; +} + +#pragma mark - + +- (void)setSelectedViewController:(NSViewController <MASPreferencesViewController> *)controller +{ + if (_selectedViewController == controller) + return; + + if (_selectedViewController) + { + // Check if we can commit changes for old controller + if (![_selectedViewController commitEditing]) + { + [[self.window toolbar] setSelectedItemIdentifier:_selectedViewController.identifier]; + return; + } + [self.window setContentView:[[NSView alloc] init]]; + [_selectedViewController setNextResponder:nil]; + // With 10.10 and later AppKit will invoke viewDidDisappear so we need to prevent it from being called twice. + if (![NSViewController instancesRespondToSelector:@selector(viewDidDisappear)]) + if ([_selectedViewController respondsToSelector:@selector(viewDidDisappear)]) + [_selectedViewController viewDidDisappear]; + _selectedViewController = nil; + } + + if (!controller) + return; + + // Retrieve the new window tile from the controller view + if ([self.title length] == 0) + { + NSString *label = controller.toolbarItemLabel; + self.window.title = label; + } + + [[self.window toolbar] setSelectedItemIdentifier:controller.identifier]; + + // Record new selected controller in user defaults + [[NSUserDefaults standardUserDefaults] setObject:controller.identifier forKey:kMASPreferencesSelectedViewKey]; + + NSView *controllerView = controller.view; + + // Retrieve current and minimum frame size for the view + NSString *oldViewRectString = [[NSUserDefaults standardUserDefaults] stringForKey:PreferencesKeyForViewBounds(controller.identifier)]; + NSString *minViewRectString = [_minimumViewRects objectForKey:controller.identifier]; + if (!minViewRectString) + [_minimumViewRects setObject:NSStringFromRect(controllerView.bounds) forKey:controller.identifier]; + + BOOL sizableWidth = ([controller respondsToSelector:@selector(hasResizableWidth)] + ? controller.hasResizableWidth + : controllerView.autoresizingMask & NSViewWidthSizable); + BOOL sizableHeight = ([controller respondsToSelector:@selector(hasResizableHeight)] + ? controller.hasResizableHeight + : controllerView.autoresizingMask & NSViewHeightSizable); + + NSRect oldViewRect = oldViewRectString ? NSRectFromString(oldViewRectString) : controllerView.bounds; + NSRect minViewRect = minViewRectString ? NSRectFromString(minViewRectString) : controllerView.bounds; + oldViewRect.size.width = NSWidth(oldViewRect) < NSWidth(minViewRect) || !sizableWidth ? NSWidth(minViewRect) : NSWidth(oldViewRect); + oldViewRect.size.height = NSHeight(oldViewRect) < NSHeight(minViewRect) || !sizableHeight ? NSHeight(minViewRect) : NSHeight(oldViewRect); + + [controllerView setFrame:oldViewRect]; + + // Calculate new window size and position + NSRect oldFrame = [self.window frame]; + NSRect newFrame = [self.window frameRectForContentRect:oldViewRect]; + newFrame = NSOffsetRect(newFrame, NSMinX(oldFrame), NSMaxY(oldFrame) - NSMaxY(newFrame)); + + // Setup min/max sizes and show/hide resize indicator + [self.window setContentMinSize:minViewRect.size]; + [self.window setContentMaxSize:NSMakeSize(sizableWidth ? CGFLOAT_MAX : NSWidth(oldViewRect), sizableHeight ? CGFLOAT_MAX : NSHeight(oldViewRect))]; + [self.window setShowsResizeIndicator:sizableWidth || sizableHeight]; + [[self.window standardWindowButton:NSWindowZoomButton] setEnabled:sizableWidth || sizableHeight]; + + [self.window setFrame:newFrame display:YES animate:[self.window isVisible]]; + + _selectedViewController = controller; + + // In OSX 10.10, setContentView below calls viewWillAppear. We still want to call viewWillAppear on < 10.10, + // so the check below avoids calling viewWillAppear twice on 10.10. + // See https://github.com/shpakovski/MASPreferences/issues/32 for more info. + if (![NSViewController instancesRespondToSelector:@selector(viewWillAppear)]) + if ([controller respondsToSelector:@selector(viewWillAppear)]) + [controller viewWillAppear]; + + [self.window setContentView:controllerView]; + [self.window recalculateKeyViewLoop]; + if ([self.window firstResponder] == self.window) { + if ([controller respondsToSelector:@selector(initialKeyView)]) + [self.window makeFirstResponder:[controller initialKeyView]]; + else + [self.window selectKeyViewFollowingView:controllerView]; + } + + // Insert view controller into responder chain on 10.9 and earlier + if (controllerView.nextResponder != controller) { + controller.nextResponder = controllerView.nextResponder; + controllerView.nextResponder = controller; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:kMASPreferencesWindowControllerDidChangeViewNotification object:self]; +} + +- (void)toolbarItemDidClick:(id)sender +{ + if ([sender respondsToSelector:@selector(itemIdentifier)]) + self.selectedViewController = [self viewControllerForIdentifier:[sender itemIdentifier]]; +} + +#pragma mark - +#pragma mark Public methods + +- (void)selectControllerAtIndex:(NSUInteger)controllerIndex +{ + if (NSLocationInRange(controllerIndex, NSMakeRange(0, _viewControllers.count))) + self.selectedViewController = [self.viewControllers objectAtIndex:controllerIndex]; +} + +- (void)selectControllerWithIdentifier:(NSString *)identifier +{ + NSParameterAssert(identifier.length > 0); + self.selectedViewController = [self viewControllerForIdentifier:identifier]; +} + +#pragma mark - +#pragma mark Actions + +- (IBAction)goNextTab:(id __unused)sender +{ + NSUInteger selectedIndex = self.indexOfSelectedController; + NSUInteger numberOfControllers = [_viewControllers count]; + + do { selectedIndex = (selectedIndex + 1) % numberOfControllers; } + while ([_viewControllers objectAtIndex:selectedIndex] == [NSNull null]); + + [self selectControllerAtIndex:selectedIndex]; +} + +- (IBAction)goPreviousTab:(id __unused)sender +{ + NSUInteger selectedIndex = self.indexOfSelectedController; + NSUInteger numberOfControllers = [_viewControllers count]; + + do { selectedIndex = (selectedIndex + numberOfControllers - 1) % numberOfControllers; } + while ([_viewControllers objectAtIndex:selectedIndex] == [NSNull null]); + + [self selectControllerAtIndex:selectedIndex]; +} + +@end diff --git a/Pods/MASPreferences/Framework/en.lproj/MASPreferencesWindow.xib b/Pods/MASPreferences/Framework/en.lproj/MASPreferencesWindow.xib new file mode 100644 index 0000000..b73fdfa --- /dev/null +++ b/Pods/MASPreferences/Framework/en.lproj/MASPreferencesWindow.xib @@ -0,0 +1,268 @@ +<?xml version="1.0" encoding="UTF-8"?> +<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10"> + <data> + <int key="IBDocument.SystemTarget">1080</int> + <string key="IBDocument.SystemVersion">12E55</string> + <string key="IBDocument.InterfaceBuilderVersion">3084</string> + <string key="IBDocument.AppKitVersion">1187.39</string> + <string key="IBDocument.HIToolboxVersion">626.00</string> + <object class="NSMutableDictionary" key="IBDocument.PluginVersions"> + <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string> + <string key="NS.object.0">3084</string> + </object> + <object class="NSArray" key="IBDocument.IntegratedClassDependencies"> + <bool key="EncodedWithXMLCoder">YES</bool> + <string>NSCustomObject</string> + <string>NSToolbar</string> + <string>NSView</string> + <string>NSWindowTemplate</string> + </object> + <object class="NSArray" key="IBDocument.PluginDependencies"> + <bool key="EncodedWithXMLCoder">YES</bool> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + </object> + <object class="NSMutableDictionary" key="IBDocument.Metadata"> + <string key="NS.key.0">PluginDependencyRecalculationVersion</string> + <integer value="1" key="NS.object.0"/> + </object> + <object class="NSMutableArray" key="IBDocument.RootObjects" id="1000"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="NSCustomObject" id="1001"> + <string key="NSClassName">MASPreferencesWindowController</string> + </object> + <object class="NSCustomObject" id="1003"> + <string key="NSClassName">FirstResponder</string> + </object> + <object class="NSCustomObject" id="1004"> + <string key="NSClassName">NSApplication</string> + </object> + <object class="NSWindowTemplate" id="1005"> + <int key="NSWindowStyleMask">11</int> + <int key="NSWindowBacking">2</int> + <string key="NSWindowRect">{{540, 400}, {360, 270}}</string> + <int key="NSWTFlags">1618478080</int> + <string key="NSWindowTitle"/> + <string key="NSWindowClass">NSWindow</string> + <object class="NSToolbar" key="NSViewClass" id="693562270"> + <object class="NSMutableString" key="NSToolbarIdentifier"> + <characters key="NS.bytes">A3419266-C6CB-4FAA-AB63-B91B70C196EA</characters> + </object> + <nil key="NSToolbarDelegate"/> + <bool key="NSToolbarPrefersToBeShown">YES</bool> + <bool key="NSToolbarShowsBaselineSeparator">YES</bool> + <bool key="NSToolbarAllowsUserCustomization">NO</bool> + <bool key="NSToolbarAutosavesConfiguration">NO</bool> + <int key="NSToolbarDisplayMode">1</int> + <int key="NSToolbarSizeMode">1</int> + <object class="NSMutableDictionary" key="NSToolbarIBIdentifiedItems"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="NSArray" key="dict.sortedKeys" id="0"> + <bool key="EncodedWithXMLCoder">YES</bool> + </object> + <reference key="dict.values" ref="0"/> + </object> + <reference key="NSToolbarIBAllowedItems" ref="0"/> + <reference key="NSToolbarIBDefaultItems" ref="0"/> + <reference key="NSToolbarIBSelectableItems" ref="0"/> + </object> + <nil key="NSUserInterfaceItemIdentifier"/> + <object class="NSView" key="NSWindowView" id="1006"> + <reference key="NSNextResponder"/> + <int key="NSvFlags">256</int> + <string key="NSFrameSize">{360, 270}</string> + <reference key="NSSuperview"/> + <reference key="NSWindow"/> + <reference key="NSNextKeyView"/> + </object> + <string key="NSScreenRect">{{0, 0}, {2560, 1418}}</string> + <string key="NSMaxSize">{10000000000000, 10000000000000}</string> + <string key="NSFrameAutosaveName"/> + <bool key="NSWindowIsRestorable">YES</bool> + </object> + </object> + <object class="IBObjectContainer" key="IBDocument.Objects"> + <object class="NSMutableArray" key="connectionRecords"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="IBConnectionRecord"> + <object class="IBOutletConnection" key="connection"> + <string key="label">window</string> + <reference key="source" ref="1001"/> + <reference key="destination" ref="1005"/> + </object> + <int key="connectionID">3</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBOutletConnection" key="connection"> + <string key="label">toolbar</string> + <reference key="source" ref="1001"/> + <reference key="destination" ref="693562270"/> + </object> + <int key="connectionID">23</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBOutletConnection" key="connection"> + <string key="label">delegate</string> + <reference key="source" ref="1005"/> + <reference key="destination" ref="1001"/> + </object> + <int key="connectionID">20</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBOutletConnection" key="connection"> + <string key="label">delegate</string> + <reference key="source" ref="693562270"/> + <reference key="destination" ref="1001"/> + </object> + <int key="connectionID">22</int> + </object> + </object> + <object class="IBMutableOrderedSet" key="objectRecords"> + <object class="NSArray" key="orderedObjects"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="IBObjectRecord"> + <int key="objectID">0</int> + <reference key="object" ref="0"/> + <reference key="children" ref="1000"/> + <nil key="parent"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">-2</int> + <reference key="object" ref="1001"/> + <reference key="parent" ref="0"/> + <string key="objectName">File's Owner</string> + </object> + <object class="IBObjectRecord"> + <int key="objectID">-1</int> + <reference key="object" ref="1003"/> + <reference key="parent" ref="0"/> + <string key="objectName">First Responder</string> + </object> + <object class="IBObjectRecord"> + <int key="objectID">-3</int> + <reference key="object" ref="1004"/> + <reference key="parent" ref="0"/> + <string key="objectName">Application</string> + </object> + <object class="IBObjectRecord"> + <int key="objectID">1</int> + <reference key="object" ref="1005"/> + <object class="NSMutableArray" key="children"> + <bool key="EncodedWithXMLCoder">YES</bool> + <reference ref="1006"/> + <reference ref="693562270"/> + </object> + <reference key="parent" ref="0"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">2</int> + <reference key="object" ref="1006"/> + <reference key="parent" ref="1005"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">4</int> + <reference key="object" ref="693562270"/> + <object class="NSMutableArray" key="children"> + <bool key="EncodedWithXMLCoder">YES</bool> + </object> + <reference key="parent" ref="1005"/> + </object> + </object> + </object> + <object class="NSMutableDictionary" key="flattenedProperties"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="NSArray" key="dict.sortedKeys"> + <bool key="EncodedWithXMLCoder">YES</bool> + <string>-1.IBPluginDependency</string> + <string>-2.IBPluginDependency</string> + <string>-3.IBPluginDependency</string> + <string>1.IBNSWindowAutoPositionCentersHorizontal</string> + <string>1.IBNSWindowAutoPositionCentersVertical</string> + <string>1.IBPluginDependency</string> + <string>1.IBWindowTemplateEditedContentRect</string> + <string>1.NSWindowTemplate.visibleAtLaunch</string> + <string>2.IBPluginDependency</string> + <string>4.IBPluginDependency</string> + </object> + <object class="NSArray" key="dict.values"> + <bool key="EncodedWithXMLCoder">YES</bool> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + <boolean value="NO"/> + <boolean value="NO"/> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + <string>{{484, 402}, {360, 270}}</string> + <boolean value="NO"/> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + </object> + </object> + <object class="NSMutableDictionary" key="unlocalizedProperties"> + <bool key="EncodedWithXMLCoder">YES</bool> + <reference key="dict.sortedKeys" ref="0"/> + <reference key="dict.values" ref="0"/> + </object> + <nil key="activeLocalization"/> + <object class="NSMutableDictionary" key="localizations"> + <bool key="EncodedWithXMLCoder">YES</bool> + <reference key="dict.sortedKeys" ref="0"/> + <reference key="dict.values" ref="0"/> + </object> + <nil key="sourceID"/> + <int key="maxID">23</int> + </object> + <object class="IBClassDescriber" key="IBDocument.Classes"> + <object class="NSMutableArray" key="referencedPartialClassDescriptions"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="IBPartialClassDescription"> + <string key="className">MASPreferencesWindowController</string> + <string key="superclassName">NSWindowController</string> + <object class="NSMutableDictionary" key="actions"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="NSArray" key="dict.sortedKeys"> + <bool key="EncodedWithXMLCoder">YES</bool> + <string>goNextTab:</string> + <string>goPreviousTab:</string> + </object> + <object class="NSArray" key="dict.values"> + <bool key="EncodedWithXMLCoder">YES</bool> + <string>id</string> + <string>id</string> + </object> + </object> + <object class="NSMutableDictionary" key="actionInfosByName"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="NSArray" key="dict.sortedKeys"> + <bool key="EncodedWithXMLCoder">YES</bool> + <string>goNextTab:</string> + <string>goPreviousTab:</string> + </object> + <object class="NSArray" key="dict.values"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="IBActionInfo"> + <string key="name">goNextTab:</string> + <string key="candidateClassName">id</string> + </object> + <object class="IBActionInfo"> + <string key="name">goPreviousTab:</string> + <string key="candidateClassName">id</string> + </object> + </object> + </object> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBProjectSource</string> + <string key="minorKey">./Classes/MASPreferencesWindowController.h</string> + </object> + </object> + </object> + </object> + <int key="IBDocument.localizationMode">0</int> + <string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string> + <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies"> + <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3</string> + <integer value="3000" key="NS.object.0"/> + </object> + <bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool> + <int key="IBDocument.defaultPropertyAccessControl">3</int> + </data> +</archive> diff --git a/Pods/MASPreferences/LICENSE.md b/Pods/MASPreferences/LICENSE.md new file mode 100644 index 0000000..3148a77 --- /dev/null +++ b/Pods/MASPreferences/LICENSE.md @@ -0,0 +1,24 @@ +MASPreferences is licensed under the 2-clause BSD license. + +Copyright (c) 2016 Vadim Shpakovski. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Pods/MASPreferences/README.md b/Pods/MASPreferences/README.md new file mode 100644 index 0000000..d9740d1 --- /dev/null +++ b/Pods/MASPreferences/README.md @@ -0,0 +1,21 @@ +# MASPreferences + +This component is intended as a replacement for SS_PrefsController by Matt Legend Gemmell and Selectable Toolbar by Brandon Walkin. It is designed to use NSViewController subclasses for preference panes. + +# How to use + +You can find a Demo project at [MASPreferencesDemo](https://github.com/shpakovski/MASPreferencesDemo). + +##Swift Edge case +When using Swift you need to override the `identifier` from `MASPreferencesViewController` the following to be compatible with the mutable identifier `String?` in `NSViewController` + + override var identifier: String? { get {return "general"} set { super.identifier = newValue} } + +# Install +#### [Carthage](https://github.com/Carthage/Carthage) + +- Add `github "shpakovski/MASPreferences"` to your Cartfile. + +#### [CocoaPods](https://github.com/cocoapods/cocoapods) + +- Add `pod 'MASPreferences'` to your Podfile. diff --git a/Pods/Manifest.lock b/Pods/Manifest.lock new file mode 100644 index 0000000..4ff5ff7 --- /dev/null +++ b/Pods/Manifest.lock @@ -0,0 +1,27 @@ +PODS: + - FeedKit (7.0.1) + - GRDB.swift (2.0) + - MASPreferences (1.2.1) + +DEPENDENCIES: + - FeedKit (from `https://github.com/CD1212/FeedKit.git`) + - GRDB.swift + - MASPreferences + +EXTERNAL SOURCES: + FeedKit: + :git: https://github.com/CD1212/FeedKit.git + +CHECKOUT OPTIONS: + FeedKit: + :commit: ebac2773014d88a14ee8cd8a8ff78332ca763592 + :git: https://github.com/CD1212/FeedKit.git + +SPEC CHECKSUMS: + FeedKit: c62e463a37f51b991b05ad0d74e217c1a59e7a2a + GRDB.swift: 2855bdf57ee5cfa30cd3fb06bc47a086fcd85962 + MASPreferences: 23946c155d77f0e72a6e5af9d8b67c52210a1fd0 + +PODFILE CHECKSUM: 6574b33010906d835b8683aa8cb5deaf7ba42e6e + +COCOAPODS: 1.3.1 diff --git a/Pods/Pods.xcodeproj/project.pbxproj b/Pods/Pods.xcodeproj/project.pbxproj new file mode 100644 index 0000000..1181e96 --- /dev/null +++ b/Pods/Pods.xcodeproj/project.pbxproj @@ -0,0 +1,2215 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 01560F02DF617E37AAEC99826AD225FD /* Fixits-0.102.0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5248BD7939C5BDDD4886DF1588A999BF /* Fixits-0.102.0.swift */; }; + 015A0EA63F06AE59C3EBA4C962B88228 /* DatabaseDateComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 909D02D461734E0F0AB95898A4024D87 /* DatabaseDateComponents.swift */; }; + 01E54B49B17CBC817DD4B67BC5AA90E9 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086D654A17B1E380BDB36A6630AFE941 /* Utils.swift */; }; + 0237D4A96BC7A1CA501D012F9BA46D79 /* AtomFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F0411FDC4E4B8F16B23018CDE02F545 /* AtomFeed.swift */; }; + 036717BC5600473E188068146F5D7293 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E110E3518E2D475577D0E2056C8B05D /* Foundation.framework */; }; + 04670CE91916C62AB41DC2C3753554A4 /* QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC9A165C781CD0AAA4E579881930270 /* QueryInterfaceRequest.swift */; }; + 063A8F9E6A28D0C87C2FEBFE1BB69232 /* RSSFeedCloud.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A313CB04131D623C721B9524002E71 /* RSSFeedCloud.swift */; }; + 063DD0F9CB22847FF37EDD6CD47121C5 /* AtomFeedContributor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43BD907312A2BC39FAE7B25ACF4E8B4 /* AtomFeedContributor.swift */; }; + 076A1ECDB671FAB22AAF6312479A349B /* MediaStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26945C000AA9D7D348898F53C3F26C6B /* MediaStatus.swift */; }; + 085B45834F82BF5A38B82B46969EF0C2 /* NSString.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E3FA503DE3C156EE18759FCBBCB92B /* NSString.swift */; }; + 088A60E704FDA697F822C63F40E3418E /* Array + Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65214CE868D1BDAE4E1AC38A8BC2C34E /* Array + Equatable.swift */; }; + 0939AF851462FFFEAE4A6AE9B7CFF25B /* AtomFeedEntryContributor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F6539C6E6E8788E661DBE3294E4E05B /* AtomFeedEntryContributor.swift */; }; + 0B29835F8D0714DCAA2466F177F80815 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48933C4790981A5B9C41C01C95C37036 /* Request.swift */; }; + 0B75D161AF26494D7FA03A54ED261F12 /* AtomFeed + mapAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D98F0F0252FBD4913F0BB83FADF61803 /* AtomFeed + mapAttributes.swift */; }; + 0D45DC3AB332EBB1E9E878092800EBE4 /* MediaGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ACD26AA6790B08049C2C449EA23CA33 /* MediaGroup.swift */; }; + 0EFBA044F184723BCF120E94B5DF5AA4 /* MediaPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA76253F7D1E4B224B809F4D43D79D0 /* MediaPrice.swift */; }; + 11CDD732FBEFFB6C222341F2E5D1FD43 /* Fixits-0-90-1.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9981D05528B0F8A77DBDB8D1C39AC2C /* Fixits-0-90-1.swift */; }; + 1238583ABBFD5367A8E09B3020116996 /* DatabaseMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83DE56D56DF7F33A58CDE3FBE5C74BC /* DatabaseMigrator.swift */; }; + 1242E894CDE5ABF50DA3CF1E629D4D0D /* Pods-DoughnutTests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 1910CDF40C4CC9713A3B43B7858153DE /* Pods-DoughnutTests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1363A9F5B7FB467080868AB86A16E081 /* RowConvertible+TableMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = BACAABB13C2049B29778580D51A8B921 /* RowConvertible+TableMapping.swift */; }; + 144BD0BDA4E1B52F2884500FE3D0DE84 /* MASPreferences-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = B9A97FF1AD255831C3D35EF762EC3963 /* MASPreferences-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 16A2421302AD3C562A22ACF05DD35D27 /* DatabaseValueConvertible+RawRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345F727AEA9953BC160BCE131BEE6427 /* DatabaseValueConvertible+RawRepresentable.swift */; }; + 1873E44626B3D7D35BB567BA2481DFEB /* RSSFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2090385CD59D9E249D286138C448874D /* RSSFeed.swift */; }; + 1D8F7BDC0B319B198D507A3896185FC1 /* FeedParserProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C5F30E5A3ABF44784B7B18B9CB38EC /* FeedParserProtocol.swift */; }; + 1E86F1FCF01FE15CBD7E6B68EF452745 /* DatabaseQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89660E86ED40BAC0E1B18F677B38D934 /* DatabaseQueue.swift */; }; + 1EC1ED271E82FAA726773B1F31781EAC /* MediaDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A6CBF8B9643CF1E26A14496283E5DA4 /* MediaDescription.swift */; }; + 1F20A25728EEBDC0604F5FB1C0DC158A /* AtomFeedEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2708E731D7937F85464E817C4F1F392 /* AtomFeedEntry.swift */; }; + 205A1AF3BB4D7FF2DCF6BB25F360EB6D /* MediaEmbed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD4C79EE9270EF704FBA5014355AD6F /* MediaEmbed.swift */; }; + 21EAE6C8681B3A0F81898E9FB403A6AD /* Persistable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C0BBC5E24A2CD358DB23464123FD09 /* Persistable.swift */; }; + 21F05C2C1255B9B61A26263765F5236E /* TableMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E970FCB53AD5114A3ED81BB895758B /* TableMapping.swift */; }; + 2356483F9343F03D920CD8EC98E3FE20 /* SQLCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A62F2BADE8667533499FF2633F916702 /* SQLCollection.swift */; }; + 235EC9451E8E3A445F36EB5B205869A7 /* AtomFeedEntryCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE3504411BBEE0B3855BA65FC602342 /* AtomFeedEntryCategory.swift */; }; + 24068B8E24BC88B37E17C39D64AD2D0E /* JSONFeedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4A2B52EFE7333221F7CF81012AC5F67 /* JSONFeedItem.swift */; }; + 2417FF905183DC1A71828FB7E609568A /* RSSFeedItemCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A375A121C95AE6A31CEE5765B1F812CF /* RSSFeedItemCategory.swift */; }; + 27F3AE895F9B086800E6E7FFF1781EDF /* ContentNamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 839000271C33C4119287E2F824A15E93 /* ContentNamespace.swift */; }; + 28A8BE7772EB0F6B66FCB719AEFF76F1 /* iTunesNamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6EBBD1E2BB17FED1D25D9FFBA4AE8DA /* iTunesNamespace.swift */; }; + 2964C38653915FDF24BB610384B7701A /* AtomFeedLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEFAB4A5D221AC58521B7FA1C9A28DE7 /* AtomFeedLink.swift */; }; + 2A0B4AF17D2210AFB43A490B8A800C6B /* MediaCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A370ECF8E9E52E231E21BD8C255242 /* MediaCategory.swift */; }; + 2CB5E4AD8990D06ED01F24CA7D0B122D /* MediaStatistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF3615C4D8B01BC8825F8C215A10CB7 /* MediaStatistics.swift */; }; + 2CEDE282E4B964FFA5E41BABFBA368A0 /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0609DED75DE6CFF2D82338555B4E5A3B /* Cursor.swift */; }; + 2D19D16295FAD50FE799CFA228073D81 /* JSONFeedAuthor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B194E721E4940F89173AFEF1A6DD3474 /* JSONFeedAuthor.swift */; }; + 2F6AA8C1835345FFF75D91065B996706 /* MediaParam.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9F3C41B84EDF9348354733659FD0FD /* MediaParam.swift */; }; + 2F7B5A9085482D838DCF6ECE0A3D137B /* MediaRights.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC234BBB7B09EBF0829EFC3CCB7F5D34 /* MediaRights.swift */; }; + 3240C422D123E107E4EB7BD424BDB0CA /* DublinCoreNamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17509CB56392BAE025F2FBB08DA5C564 /* DublinCoreNamespace.swift */; }; + 32C4181080FE75CECF7FD5A31906CD25 /* RSSFeedSkipHour.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6868B56A9CF1EA8B774C19F1E312FB0 /* RSSFeedSkipHour.swift */; }; + 3362791404D89590C03CED865758AC08 /* JSONFeedAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = A844E23A4174EE027F5829C70BE84605 /* JSONFeedAttachment.swift */; }; + 37A6415DA362F829BF608437FC03CB1F /* RSSPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FFD7DF4FB94A15BB0B157D8A1D2E5FE /* RSSPath.swift */; }; + 39118E3F66EE193B092373438388337D /* RSSFeed + mapAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B09ADF172A609B9FDC957B61799FE018 /* RSSFeed + mapAttributes.swift */; }; + 3B9E7A56D2799317BD22BFF154457C60 /* Data + toUtf8.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D0ED7D249D7FF63E2F970EDCD563F0 /* Data + toUtf8.swift */; }; + 3BDFB8591F0C22E33614B68B83FBE2EF /* AtomFeedEntrySummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 092B20E7ED758074C1F57B5E8C2CAA7F /* AtomFeedEntrySummary.swift */; }; + 3BED62B38E4739DF3B70CB3B19A03DBC /* StandardLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 655D5D910E7C1570B9ACB2326AAFD7B0 /* StandardLibrary.swift */; }; + 3C40EDBD4D8BF1AFB414770B4FC2AC85 /* String + toBool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BDC0D2CCBE0F8F1A76E5D16D4768348 /* String + toBool.swift */; }; + 3CB6C7436E0C5CCBBA6FE4A3C3A059F8 /* RSSFeedSkipDay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DEC08EF42EEC705FE3CFB01E22D920 /* RSSFeedSkipDay.swift */; }; + 3E70411BE2F06354BB65A6712D17BE18 /* DatabaseFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFCCD4563D2890C2F24004E30E87F652 /* DatabaseFunction.swift */; }; + 3EE1F8E4698066084D9CAC51FDEF53C6 /* MediaThumbnail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799DB69FD68D658230A61F2D16A03C6F /* MediaThumbnail.swift */; }; + 40010EF45358B85D9197E80F8BD64E0C /* RowConvertible+Decodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0147059819728AFACFC2E4BE809B235 /* RowConvertible+Decodable.swift */; }; + 4108F7B95ADE8A41A3832AFF6EC37BF9 /* XMLFeedParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FCF66BA1E5084A1E84E02F4DF36721C /* XMLFeedParser.swift */; }; + 424DE7EC40EE4D81431855BB3DB720CE /* SQLSelectable+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B353141BFC1CCF8446F81E09DE99C8 /* SQLSelectable+QueryInterface.swift */; }; + 4260E8B2EA1EB476EF1329D326ABED3C /* Pods-DoughnutUITests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 70A3F74B9D0C5554C6464A91B04FC614 /* Pods-DoughnutUITests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 428B6EC43A65FA2BCB01E38583D6F9E0 /* Record.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4728069A0AA3F69C0DAA76DC03FE5AB8 /* Record.swift */; }; + 42D64C35C66C24D2B0E931ADB7ACF8B2 /* SQLSelectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D34890E940700B6CBB4372925A5629E /* SQLSelectable.swift */; }; + 44FE6263FAE7839D8844B8891CF5B012 /* RowAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D9FFD14F2949364162F5568B74093E /* RowAdapter.swift */; }; + 45F03DD3DF08776DFC2B50BC7032BA52 /* FTS3Pattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14A4FE5E0F0883B56473CE2D57627E58 /* FTS3Pattern.swift */; }; + 472C7BE6804B003089D20FA686CCD1B5 /* DatabaseWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62BFB7CBCB7EB2A55B3943699D8E30D8 /* DatabaseWriter.swift */; }; + 4C7D6B8EB9A400F6102DB2B26C18C0DC /* iTunesCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92949F3008F8A36A622F68FD7459C8E0 /* iTunesCategory.swift */; }; + 4DF4FBD1BA874A79A16B4B3EDECCC2BC /* FTS3TokenizerDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCAD435CDE8E1DA04D186B5C30D21ADF /* FTS3TokenizerDescriptor.swift */; }; + 4EB569E625B83FD452793CD5B3EA2214 /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4085AE79FDBE5C4FDD656E2C3580CFF3 /* Migration.swift */; }; + 54681616749A98E8392A5C914A465B30 /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D739B440A5ABF34D83780606966390A /* FTS4.swift */; }; + 57C22769BBBCF1C5B827023A266EFC04 /* SerializedDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA052A5ED7F6E02FB674087C3029623 /* SerializedDatabase.swift */; }; + 586C16521FF293184DE95268EC61B8FC /* MediaScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15B317C4BA95971FEB55DFD99FD62B88 /* MediaScene.swift */; }; + 58F773BE75A892FF637F10EC40B35FBB /* iTunesOwner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ABA0500029338AAB24476CD0AC60636 /* iTunesOwner.swift */; }; + 5B9FA1758B40DC061FE2CCBBBD1C0FF7 /* SQLExpression+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5349F246A8B014B5AD1551E3BD7E4A2D /* SQLExpression+QueryInterface.swift */; }; + 5C204DCE823928C66C91455E6C07E02E /* RSSFeedItemGUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 155D2F785AC51B9BE3285E8D588D263F /* RSSFeedItemGUID.swift */; }; + 5C229A7FF23962E233C9D76A8AF625CA /* MediaTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E65D049C763D17342C51AA684A137C6 /* MediaTag.swift */; }; + 5C2F6E3CAFB6E9B5F6038C410E27DCB4 /* AtomFeedEntryAuthor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF263A347785618E1B42B7909071E59 /* AtomFeedEntryAuthor.swift */; }; + 5E2325376771642437CE5C67781DE7BF /* DatabaseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D69051CE87133DF7943048BB905865C /* DatabaseError.swift */; }; + 5F6657A6965F6AA0CFEF516650EF8AA9 /* QueryInterfaceSelectQueryDefinition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97BEAA6413FFCB46AC6B51770FE8C168 /* QueryInterfaceSelectQueryDefinition.swift */; }; + 61B397687EE640E035029E53126EE94C /* MASPreferencesViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = AC666F41DE4D65527C1C6C54BBC061DA /* MASPreferencesViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 61C19C012C3834BA7ED7DE2618CFADC2 /* RSSFeedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF126CA2ABB0DF935BA3160D49EA1C /* RSSFeedImage.swift */; }; + 6220B4E4C3A170FDE480BC6392A3A096 /* GRDB.h in Headers */ = {isa = PBXBuildFile; fileRef = C0FD700528DE5AC13168918C20F51190 /* GRDB.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 644B7F42B2B174F0D4B2F23981E9A914 /* RFC3339DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4440A47EAF0EB92EBF82AD926AB818DF /* RFC3339DateFormatter.swift */; }; + 6807BEB050DDF6C95D15F5BE298D0912 /* AtomFeed + mapCharacters.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8FADD9E1FC21D7D047AC33F719E1EB2 /* AtomFeed + mapCharacters.swift */; }; + 692C5264C6FFDA0BD31C442D85353A13 /* AtomPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F5AC5350AFA457332A5BE79141EDAB8 /* AtomPath.swift */; }; + 6ADE7E972B318F1BF25360B911A4A2E1 /* SQLOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEC4BD3E87F3F9A8ED67476718C8BE6C /* SQLOperators.swift */; }; + 6B1758A594C156CBF6FE5243BDAB629F /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5E2EA9C94BAAD2DF20F479AD4B21DE /* Result.swift */; }; + 6BA8FB4777E72CCAC403764A560C8224 /* AtomFeedSubtitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC920E72A2A8627ECFF1E5A2CFAD186B /* AtomFeedSubtitle.swift */; }; + 6BFEF476AD33FE4A90F4292277CDB12F /* MediaContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E72CA31472884A973CA9402D7A4B823 /* MediaContent.swift */; }; + 6C7660830812AB978AAA6891A856829F /* Pool.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB282CFF6640F828AC6C371DD1F161D4 /* Pool.swift */; }; + 6D6CA410EFDE19B29F6FCBB5B461A065 /* AtomFeedEntrySource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A2208AB57BE1FEBE9255D832D7D7B4 /* AtomFeedEntrySource.swift */; }; + 6D7D4A8899C29A405C216F77B65AC572 /* XMLFeedType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B9D3DD609D42338F2E257E2BF9EAFD /* XMLFeedType.swift */; }; + 6F058921789AA8851CC4A16EB141F8F7 /* MediaCopyright.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA324DD169521C25B580FBB406FB339 /* MediaCopyright.swift */; }; + 6FA4DEDE899D108F6BCAA16CCFCFA1FC /* MediaHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F04F3CE523623FA28164FCDEBC53B4A /* MediaHash.swift */; }; + 701BD3E86F3478C8F4B19565FDB78206 /* ReadWriteBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DCAC7083C7CF2E9D85BB9C7600139F2 /* ReadWriteBox.swift */; }; + 70B4649FF85E84EF23F63F1A96DBD103 /* Fixits-0-84-0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8611935157B6EBA420E2C339F1C99B7D /* Fixits-0-84-0.swift */; }; + 71C6DACF27FC54A115C50456E0D07512 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8876D2FE66CCB91AD8DEF3597EB66A7 /* Result.swift */; }; + 723DCDFAD209722869C4D46DE9F45527 /* grdb_config.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F539FFB32290F4DFF61D1DD8198EFAE /* grdb_config.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 73D7DE4E3DA284B129ECC2881C1AA74C /* MASPreferencesWindowController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8993969726B6814C37AE798578562B77 /* MASPreferencesWindowController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 75150B06211DDFE2B4E9545D3C385661 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 093AB0FF5E4B611961E2CC5C302DF649 /* URL.swift */; }; + 7837556CF0F4AB4A4C1DE19C2E6EC6AC /* AtomFeedGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 273F49BE94FFE15CC030E0A6367D7452 /* AtomFeedGenerator.swift */; }; + 7A0F55BD37EF8A2301DDA48D3669C734 /* NSNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 145F60F2F4B4ADC0150F858314325F83 /* NSNumber.swift */; }; + 7AAB56C1560351BE05625EFFDD864E67 /* SQLCollatedExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB717BC79D7B38246221EFE3E43E0FAB /* SQLCollatedExpression.swift */; }; + 7D3461BE566C3D6B6EBC80833CDCC270 /* DatabaseReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 808D59870017B81D7C4BB491E46E2FB5 /* DatabaseReader.swift */; }; + 7EDFAFF87720EC79FE90C828C9E50808 /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B3E456071D498DCDDFB074B684C03E /* Statement.swift */; }; + 7F31A833F094F10E992B895E43B1BF5C /* MediaLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C0C15882F9128409587BC60986CAE6 /* MediaLocation.swift */; }; + 800DECB7F8255CF8E62D7F9B035E7513 /* JSONFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BAE4D76B6147E2349A91CA1B281605 /* JSONFeed.swift */; }; + 82DD7212EF146FB98A06666B4B1DE0B7 /* TableDefinition.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6EFBAB00C4728232E692083A01126BE /* TableDefinition.swift */; }; + 8318410374932897227C4A711533CB38 /* RDFPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54735836C007714DAC3D8D4C506D3240 /* RDFPath.swift */; }; + 84A262BF780BF7D1E23F54AFF778D406 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6526E38143AA3C7320583E142AA64538 /* Cocoa.framework */; }; + 850B1AA8CAE2960B8932379904BA70ED /* FetchedRecordsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05C32152421CEA4C13D11CA19C3627A7 /* FetchedRecordsController.swift */; }; + 863B5BC440F1B41F8E2C3A8DFDED4303 /* FeedParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5617C9611D7032C96074A4E4B9E1DF3B /* FeedParser.swift */; }; + 86EFFB72730BF178331CFB823A40C2FA /* FTS5CustomTokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F9437AC3C5900FB1C5F0F23FA8DF24E /* FTS5CustomTokenizer.swift */; }; + 87B89ABA2215F31972915C945A4B15BF /* SchedulingWatchdog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6081E2379FA9B56747171B94180401 /* SchedulingWatchdog.swift */; }; + 88B83D679F350AD0B76B5EE328FE4888 /* VirtualTableModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A744578CEA7FCC611331AC7A16ACA2AB /* VirtualTableModule.swift */; }; + 89853C218B46B1E6CC59A9775623AD0C /* Persistable+Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 756CD83642C176613222ED4F6407FA4E /* Persistable+Encodable.swift */; }; + 8A8AC3C2B2EE1545F308684E8B159667 /* Fixits-0.101.1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAF175727EB0D921FEE0B80A6BC1870 /* Fixits-0.101.1.swift */; }; + 8ABB550E3E61D643C7E9EE85A7D0DA32 /* MASPreferencesWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0A6DC69A17F3FE58408AE4DCED195A96 /* MASPreferencesWindow.xib */; }; + 8B5DFC093DE87C292BB89F1A07255592 /* DatabaseValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 990043F5B82A0419C8681BFC6DCF3BA5 /* DatabaseValue.swift */; }; + 8C0E07823C530623AEEA281DA56A7944 /* FTS5Tokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF403126D6AD8F4F71602596FB86C4B7 /* FTS5Tokenizer.swift */; }; + 8C0F573FD1B7ECFA566ACE26D85214C8 /* FeedDataType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3626575071AD091BD4BE691359B7C1A0 /* FeedDataType.swift */; }; + 8CA1D287612D1D8DDA3A6B1B712F9474 /* iTunesImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76057D84DFD5E2851EC4FE792E0EE6B /* iTunesImage.swift */; }; + 8D37FC0CEF5D1351AA18A552AC5ED229 /* JSONFeedParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A4B8183704A49BA77BC4E0E093D009 /* JSONFeedParser.swift */; }; + 8D96AD1FE2E42E51F4C219E05B2BE1F4 /* RSSFeedCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D328195E55287400DD1F7E05DEFDDF2F /* RSSFeedCategory.swift */; }; + 8EC3B36BFD00D14373F3AF46BBE04275 /* FTS5Pattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B6579C6525893DF3C918F55CA5100B /* FTS5Pattern.swift */; }; + 8F0CDA7E86B37D92A413FFFF6A97B526 /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9878D2CA813DFF2A735ED27855C6F95 /* UUID.swift */; }; + 8FD9329BFD54F9F18990F41446F7ED65 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA830A16FECE6E7A873C6709E18C12C8 /* Data.swift */; }; + 90564EF1A72FE9DF7EBCC06AA9F16411 /* MediaRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01647BDBF7E7728E44DE1290E9D7331C /* MediaRating.swift */; }; + 924C41137EC5BE2FF670AF5DB2450491 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB0E864CE99D47F0FFC7C0A97D12D26C /* Date.swift */; }; + 9366DF79DD0869EDAC920306F1B03ECE /* FTS3+QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F16B07D6738B2921FC72E05C59FBA928 /* FTS3+QueryInterfaceRequest.swift */; }; + 9420B9A41A54B52A10E74B42CFA732EF /* MASPreferences-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 15E24F59F5BF1D93B88559CE6FDCD69D /* MASPreferences-dummy.m */; }; + 945F7D9D42E99827AE3020683AC7D4A9 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6526E38143AA3C7320583E142AA64538 /* Cocoa.framework */; }; + 97322C178DADD18D0871E4697684E7C6 /* Row.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8A49C0DDB44E19ED62DD5DAED3C37B1 /* Row.swift */; }; + 9816499E2B73B99BD8E9BF60EA2EF9F5 /* AtomFeedEntryContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3940F027FB8051D776545A5FAFEE14E /* AtomFeedEntryContent.swift */; }; + 99F493FA1DB73B54ACF8F8A57514571E /* SyndicationNamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1556B2332BE75D499CF0A40BE0C1966 /* SyndicationNamespace.swift */; }; + 9A17507AB3A58585BDF151E7E6309DED /* ISO8601DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E9440E9227ED19949687BC0C4EC028 /* ISO8601DateFormatter.swift */; }; + 9C2E7D4708BEDF7064F62626C0F9547F /* MediaTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077BBF7D76B836ADD3E6C522FD4E0CC6 /* MediaTitle.swift */; }; + 9FA8120D435E6B6C75A94B32A2DD7DAD /* FeedKit-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = EFFD3F77AA195135E9815DB402EE79BC /* FeedKit-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A0003DE469543BD0F0E90311114A68D0 /* Column.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2008EFF5E8CD78BF04F92700384661BB /* Column.swift */; }; + A1279CA0A2D65F19B122B64C523527CD /* Fixits-0.109.0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ED9EA722D4F1776976D4BFBEB0BBC60 /* Fixits-0.109.0.swift */; }; + A4977E51D9C9B6FD78141228BF959FF7 /* SyndicationUpdatePeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E70452D49A78D4A4705F0BB5BB51363 /* SyndicationUpdatePeriod.swift */; }; + A55792C74416B10B3BED5E3F152C237D /* AtomFeedCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB082807ACF8CF6B11D0B2FC78F324A /* AtomFeedCategory.swift */; }; + A8079AD54B0441CC55B1AA06140B17D0 /* Fixits-Swift2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507BBD5E05AFFB3CCEC645B291402119 /* Fixits-Swift2.swift */; }; + A81811A901C50179B355C866545761EB /* JSONFeedHub.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0099E7DEAC48069E5DCCF8235795E55 /* JSONFeedHub.swift */; }; + A8844D4934B07CFFF5BF7E27A2CEB3EA /* SQLSpecificExpressible+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 942965EB959AD61EFD82EA92F993292C /* SQLSpecificExpressible+QueryInterface.swift */; }; + A89ACFFC97857A07F5A5A7A5469DE628 /* RSSFeedItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD282198E99B8CEB3193D8386D4569E0 /* RSSFeedItemSource.swift */; }; + A8B88DCBBAD0A2561D5259E449BBF39C /* MediaLicence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DE1F6A45202874E735B0FA709AD787F /* MediaLicence.swift */; }; + A9EA14058E1624F4F9ECB170A15D2EA4 /* GRDB-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = 0221306196EB6FE75412784C2196C5CA /* GRDB-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AACD2D2061D7058C56DB176C5A814393 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6526E38143AA3C7320583E142AA64538 /* Cocoa.framework */; }; + AB3907FEDFD269386C02BB2B01E4693C /* MediaNamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BCD90EFCB2BF6BC146A94E64F28B7CB /* MediaNamespace.swift */; }; + ACB40D7CEC3FDB849FD234ED4424AFCE /* Pods-DoughnutTests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 02DB42D9A5AEF3D105C570519A087040 /* Pods-DoughnutTests-dummy.m */; }; + AD1B1B307DE44CAB2C11625292078CE8 /* MediaStarRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8347C6476E00478B07A624C646C6736D /* MediaStarRating.swift */; }; + ADA3B0939DA8061C5DB29BF6F3A335B4 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6526E38143AA3C7320583E142AA64538 /* Cocoa.framework */; }; + AE3914F24305DA71BEBF5FA40FCA4A3A /* Fixits-1.2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911F15D7F42F8B451A0AB8DCCBDED5FB /* Fixits-1.2.swift */; }; + AEC27B0DE32675E1CE6CAA2BA2D269DE /* SQLFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F5F59BC5036BDA8AFC885A3153B8CCD /* SQLFunctions.swift */; }; + AF4303F69D4D487662B7421C51327EBF /* RSSFeedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 626BFAFCFE80317D154CC9D0F3E04B44 /* RSSFeedItem.swift */; }; + B0BA19538AA0574BEB853C901707C2D8 /* DatabaseValueConvertible+ReferenceConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F91BDEB2D405F45EC2D361057B16F7D /* DatabaseValueConvertible+ReferenceConvertible.swift */; }; + B0F6C08D31FCA77339FBE95174C281D2 /* Pods-Doughnut-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 5F570D72DB5F4A8CB79E6FDB1A647E42 /* Pods-Doughnut-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B33BBB7886EB9FBF911A7883353F95AD /* FTS5WrapperTokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF5181AF1444B08132A17637E28D5255 /* FTS5WrapperTokenizer.swift */; }; + B3E72A5CA670E311394B99C9DF2B6079 /* CGFloat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97B6DC785F4AF05D64662BF84DF8A4C4 /* CGFloat.swift */; }; + B4BF75D727B278149028A2EA78F544A1 /* FTS3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE2A6C2A6D0C8752CF6155EF89A9F32 /* FTS3.swift */; }; + B4D22075D83F92FC37723C4B7F007FD3 /* MASPreferencesWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = E763DAE78E191A7E48589E1A4709B76B /* MASPreferencesWindowController.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; + B5D830E9CACC53DD91DE33F64E286996 /* MediaSubTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00E9A76B73910290BD0454CAF82739C9 /* MediaSubTitle.swift */; }; + B770C4BE33B1B0F277B1E230D5DD21B1 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6526E38143AA3C7320583E142AA64538 /* Cocoa.framework */; }; + B8C5EB4FACF9CCB23B8D89F6DEFA7B1F /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9753E560EAA431257EA6A208A6189F1C /* Database.swift */; }; + B8C67017FBAFAE4E9BB374ED6B9B937A /* DatabaseValueConvertible+Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C2D1354EAEFF0B665ACC2ADFE3B9025 /* DatabaseValueConvertible+Encodable.swift */; }; + B97A254403232FA34480F353A94203F0 /* RFC822DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F7654AEF8B4CFD06D6036EB8B40D28 /* RFC822DateFormatter.swift */; }; + BDE762D040D3F2C35C371861C892FB19 /* Pods-Doughnut-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = BEB0649B2A1A987642397C1C4E8D6133 /* Pods-Doughnut-dummy.m */; }; + BE15CFBF5D40F77DD63F42CEBDE5768F /* AtomFeedEntryLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = BED223170225B1433D32CDB60E49296E /* AtomFeedEntryLink.swift */; }; + BF428B844532BB985EE026AB53DE45C2 /* StatementColumnConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACFB5649B721298AFD991FE3B9B45887 /* StatementColumnConvertible.swift */; }; + C07ADD6F4B131C7545AE75B7E660102C /* RSSFeed + mapCharacters.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF1428D9482CFAAF26CF31E67ACB2BB3 /* RSSFeed + mapCharacters.swift */; }; + C335862F48E6D02D099DFCFA3D94DCC4 /* RSSFeedTextInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402E1F03C653BC276A59FA62C0315ADA /* RSSFeedTextInput.swift */; }; + C47C1EF09C23D5B2495D8BC790BEBB39 /* MediaRestriction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06777E6CFC608C7E445AD4CF0DFAA689 /* MediaRestriction.swift */; }; + C4C7790778E15ADC51D3DF9FCF2052D6 /* FeedKit-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 6EF86E191DEBE5C06D6D5B8DAA56325F /* FeedKit-dummy.m */; }; + CB5BEE1764A8BBF98ABC9BFD1970E31D /* DatabaseValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFFBF27EB0F493F55534CDAA47439E6F /* DatabaseValueConvertible.swift */; }; + CC84C111E8758AC11758D20FDE5A3E50 /* GRDB.swift-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 16276F1176B56EFC5E0F542CED393716 /* GRDB.swift-dummy.m */; }; + CFEEB09ADFF8D1D31138D90CF63DEB8B /* DatabaseSchemaCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10F7E72A3C0476B5CBE7BCD76AEB544 /* DatabaseSchemaCache.swift */; }; + D0A187A46E4282A6D6DD477A16493C9B /* MediaText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123366D60146A2DE2CDCC1D9989E3588 /* MediaText.swift */; }; + D121C36948619925BF790E794251A96A /* MediaCredit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A747BFEE7725184A738A4AD8214B33EA /* MediaCredit.swift */; }; + D1B748E576E60967CCF12A0A3D94E684 /* DateSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC625877D66709B8ADE6A7540DF37FD /* DateSpec.swift */; }; + D2E4F288BB8578A422530E91A32EF566 /* MASPreferences.h in Headers */ = {isa = PBXBuildFile; fileRef = EEA945B026139344D59E2C046CA046BE /* MASPreferences.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D40BA69B99E2928474C4728EF09159AF /* MediaPeerLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E5C3097E6A6E483EDDB3AF1AC441CE2 /* MediaPeerLink.swift */; }; + D54011128F08B3915EAF46BCC929910B /* MediaPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F5D47B7D940D112D101AC6252E1F39 /* MediaPlayer.swift */; }; + D63874368F0B2BEE40B9656EFC9149F2 /* FTS5+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = D970DD209D94BFB0FF1104FE93E63CE3 /* FTS5+QueryInterface.swift */; }; + DA1C4F13F6B316318E91CC19B9AEA5DE /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6526E38143AA3C7320583E142AA64538 /* Cocoa.framework */; }; + DCCC8F501653AF1E43791DD8D351B4B5 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9E910D1B4AE2A570378717CD26F6B0 /* FTS5.swift */; }; + DFD4B4752557AF53513FAC771204B82E /* SQLExpressible.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3ABD05CDD021FE5537B23CEFF951FDC /* SQLExpressible.swift */; }; + E126FE58CF572A77B7F6EF9E076EA7B7 /* NSData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370626C546416F774136476700DE1038 /* NSData.swift */; }; + E4D76E6F4E0988D1B5979690E37CA564 /* String + toDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B9AFD80AED75987D51508DF02F75D4 /* String + toDate.swift */; }; + E735F43DA1FF0E5F476787D414F1DDB9 /* DatabaseValueConvertible+Decodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5303BAC79922938BD120C6DC267F32C3 /* DatabaseValueConvertible+Decodable.swift */; }; + E9066FA88EFD33C6D338B56F835C9928 /* iTunesSubCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9A219DAC909D640A057B61485EAA4C /* iTunesSubCategory.swift */; }; + E91553565A99BD2277DFB0233EDF196E /* FTS5TokenizerDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66B654975C4A527AB75CCABCDE0A00DA /* FTS5TokenizerDescriptor.swift */; }; + EA1514C8236240D51A4A3775A3BD8E45 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE55E91405466A2B73473046565A296 /* Configuration.swift */; }; + EA71047F476046F5BBD51CEB6E14C928 /* NSNull.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A736DEC1453B4125EFE28FCACE033A4 /* NSNull.swift */; }; + EB8967A64C01119DFEAAE338CB985F7B /* Pods-DoughnutUITests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 49187B7A0E9B59822C04AEAAF8806B3D /* Pods-DoughnutUITests-dummy.m */; }; + EC906E518EFC6E595257D5E773A88E21 /* ParserError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE2192D7F604E57CDD62100745633A5 /* ParserError.swift */; }; + F040E39758DD812809278E04584619A4 /* SQLExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591897BEA10D71FACD9D3ED7EFF88424 /* SQLExpression.swift */; }; + F4A61C4AA6DD8E5B3C9D24CA1AD7D4B7 /* RowConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A783B2C6FB5AFD062C9546C0AFEE50 /* RowConvertible.swift */; }; + F4D96A1919175849870478C68E87EC16 /* AtomFeedAuthor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 988EE2ACD7FF2D0A5D8B413950E68C3C /* AtomFeedAuthor.swift */; }; + F8A43534BA2F83852C8D27FE18854020 /* RSSFeedItemEnclosure.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAD4E054102D819310D5AAA60A3A0C3 /* RSSFeedItemEnclosure.swift */; }; + F8B67FE870F0A4C2800F9C708CB9C0FC /* SQLOrdering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F54ABE4B6F59407C9EAC318D1E30E33 /* SQLOrdering.swift */; }; + FB73B7CE29C4591930FC99D1FAD85C39 /* MediaCommunity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6389DA01E0ED3F7107896FF7AB2BB33E /* MediaCommunity.swift */; }; + FE262D94E2F1F2752123D4785345D92F /* DatabasePool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D0D878085ECFDC95DB94F95C38CEF33 /* DatabasePool.swift */; }; + FFC2EBC4D36B69DF40D37AC0AB8DEE62 /* String + toDuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B141710B0BCDE875436149CB37450704 /* String + toDuration.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 290B43CF676979C282C868920F519984 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D41D8CD98F00B204E9800998ECF8427E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 27028E91F40BA717A57CDBFF74AD5FB8; + remoteInfo = GRDB.swift; + }; + 3E25442A23E392C08E1C9B2CCD240D2B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D41D8CD98F00B204E9800998ECF8427E /* Project object */; + proxyType = 1; + remoteGlobalIDString = D9909DAA479A84B0AA45A9DF104A5B5B; + remoteInfo = FeedKit; + }; + 81E5B64DAEC450F071399B80220563DC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D41D8CD98F00B204E9800998ECF8427E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 24407B2D21B5CBB69BA7413CA12B6DFD; + remoteInfo = MASPreferences; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 00E9A76B73910290BD0454CAF82739C9 /* MediaSubTitle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaSubTitle.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaSubTitle.swift; sourceTree = "<group>"; }; + 01647BDBF7E7728E44DE1290E9D7331C /* MediaRating.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaRating.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaRating.swift; sourceTree = "<group>"; }; + 0202316A0F9E2D5123231CC9B185FA48 /* Pods-DoughnutUITests.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-DoughnutUITests.test.xcconfig"; sourceTree = "<group>"; }; + 0221306196EB6FE75412784C2196C5CA /* GRDB-Bridging.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "GRDB-Bridging.h"; path = "Support/GRDB-Bridging.h"; sourceTree = "<group>"; }; + 02DB42D9A5AEF3D105C570519A087040 /* Pods-DoughnutTests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-DoughnutTests-dummy.m"; sourceTree = "<group>"; }; + 05C32152421CEA4C13D11CA19C3627A7 /* FetchedRecordsController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FetchedRecordsController.swift; path = GRDB/Record/FetchedRecordsController.swift; sourceTree = "<group>"; }; + 0609DED75DE6CFF2D82338555B4E5A3B /* Cursor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Cursor.swift; path = GRDB/Core/Cursor.swift; sourceTree = "<group>"; }; + 06777E6CFC608C7E445AD4CF0DFAA689 /* MediaRestriction.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaRestriction.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaRestriction.swift; sourceTree = "<group>"; }; + 06BAE4D76B6147E2349A91CA1B281605 /* JSONFeed.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = JSONFeed.swift; path = Sources/FeedKit/Models/JSON/JSONFeed.swift; sourceTree = "<group>"; }; + 077BBF7D76B836ADD3E6C522FD4E0CC6 /* MediaTitle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaTitle.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaTitle.swift; sourceTree = "<group>"; }; + 086D654A17B1E380BDB36A6630AFE941 /* Utils.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Utils.swift; path = GRDB/Utils/Utils.swift; sourceTree = "<group>"; }; + 092B20E7ED758074C1F57B5E8C2CAA7F /* AtomFeedEntrySummary.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedEntrySummary.swift; path = Sources/FeedKit/Models/Atom/AtomFeedEntrySummary.swift; sourceTree = "<group>"; }; + 093AB0FF5E4B611961E2CC5C302DF649 /* URL.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = URL.swift; path = GRDB/Core/Support/Foundation/URL.swift; sourceTree = "<group>"; }; + 0A736DEC1453B4125EFE28FCACE033A4 /* NSNull.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NSNull.swift; path = GRDB/Core/Support/Foundation/NSNull.swift; sourceTree = "<group>"; }; + 0BDC0D2CCBE0F8F1A76E5D16D4768348 /* String + toBool.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "String + toBool.swift"; path = "Sources/FeedKit/Extensions/String + toBool.swift"; sourceTree = "<group>"; }; + 0E72CA31472884A973CA9402D7A4B823 /* MediaContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaContent.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaContent.swift; sourceTree = "<group>"; }; + 0F0411FDC4E4B8F16B23018CDE02F545 /* AtomFeed.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeed.swift; path = Sources/FeedKit/Models/Atom/AtomFeed.swift; sourceTree = "<group>"; }; + 0F5F59BC5036BDA8AFC885A3153B8CCD /* SQLFunctions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SQLFunctions.swift; path = GRDB/QueryInterface/Support/SQLFunctions.swift; sourceTree = "<group>"; }; + 11C0C15882F9128409587BC60986CAE6 /* MediaLocation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaLocation.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaLocation.swift; sourceTree = "<group>"; }; + 123366D60146A2DE2CDCC1D9989E3588 /* MediaText.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaText.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaText.swift; sourceTree = "<group>"; }; + 145F60F2F4B4ADC0150F858314325F83 /* NSNumber.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NSNumber.swift; path = GRDB/Core/Support/Foundation/NSNumber.swift; sourceTree = "<group>"; }; + 14A4FE5E0F0883B56473CE2D57627E58 /* FTS3Pattern.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FTS3Pattern.swift; path = GRDB/FTS/FTS3Pattern.swift; sourceTree = "<group>"; }; + 14E554C4DFEBD9EFE559F22253712B21 /* Pods-Doughnut.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Doughnut.test.xcconfig"; sourceTree = "<group>"; }; + 155D2F785AC51B9BE3285E8D588D263F /* RSSFeedItemGUID.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedItemGUID.swift; path = Sources/FeedKit/Models/RSS/RSSFeedItemGUID.swift; sourceTree = "<group>"; }; + 15B317C4BA95971FEB55DFD99FD62B88 /* MediaScene.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaScene.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaScene.swift; sourceTree = "<group>"; }; + 15E24F59F5BF1D93B88559CE6FDCD69D /* MASPreferences-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "MASPreferences-dummy.m"; sourceTree = "<group>"; }; + 16276F1176B56EFC5E0F542CED393716 /* GRDB.swift-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "GRDB.swift-dummy.m"; sourceTree = "<group>"; }; + 17509CB56392BAE025F2FBB08DA5C564 /* DublinCoreNamespace.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DublinCoreNamespace.swift; path = "Sources/FeedKit/Models/Namespaces/Dublin Core/DublinCoreNamespace.swift"; sourceTree = "<group>"; }; + 18A370ECF8E9E52E231E21BD8C255242 /* MediaCategory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaCategory.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaCategory.swift; sourceTree = "<group>"; }; + 1910CDF40C4CC9713A3B43B7858153DE /* Pods-DoughnutTests-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-DoughnutTests-umbrella.h"; sourceTree = "<group>"; }; + 19EEA5C8D80464C5B246C7701C942623 /* Pods-DoughnutUITests.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; path = "Pods-DoughnutUITests.modulemap"; sourceTree = "<group>"; }; + 1BC625877D66709B8ADE6A7540DF37FD /* DateSpec.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DateSpec.swift; path = Sources/FeedKit/Dates/DateSpec.swift; sourceTree = "<group>"; }; + 1D8A551FBE9CBF40BB5D7CE0D7EDD04D /* MASPreferencesWindow.xib */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.xib; name = MASPreferencesWindow.xib; path = en.lproj/MASPreferencesWindow.xib; sourceTree = "<group>"; }; + 1E110E3518E2D475577D0E2056C8B05D /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + 1E5C3097E6A6E483EDDB3AF1AC441CE2 /* MediaPeerLink.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaPeerLink.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaPeerLink.swift; sourceTree = "<group>"; }; + 1F9437AC3C5900FB1C5F0F23FA8DF24E /* FTS5CustomTokenizer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FTS5CustomTokenizer.swift; path = GRDB/FTS/FTS5CustomTokenizer.swift; sourceTree = "<group>"; }; + 1FF3615C4D8B01BC8825F8C215A10CB7 /* MediaStatistics.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaStatistics.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaStatistics.swift; sourceTree = "<group>"; }; + 2008EFF5E8CD78BF04F92700384661BB /* Column.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Column.swift; path = GRDB/QueryInterface/Column.swift; sourceTree = "<group>"; }; + 2090385CD59D9E249D286138C448874D /* RSSFeed.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeed.swift; path = Sources/FeedKit/Models/RSS/RSSFeed.swift; sourceTree = "<group>"; }; + 23A783B2C6FB5AFD062C9546C0AFEE50 /* RowConvertible.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RowConvertible.swift; path = GRDB/Record/RowConvertible.swift; sourceTree = "<group>"; }; + 26945C000AA9D7D348898F53C3F26C6B /* MediaStatus.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaStatus.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaStatus.swift; sourceTree = "<group>"; }; + 273F49BE94FFE15CC030E0A6367D7452 /* AtomFeedGenerator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedGenerator.swift; path = Sources/FeedKit/Models/Atom/AtomFeedGenerator.swift; sourceTree = "<group>"; }; + 2BCD90EFCB2BF6BC146A94E64F28B7CB /* MediaNamespace.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaNamespace.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaNamespace.swift; sourceTree = "<group>"; }; + 2D2FCCB544CB76805D48CCA928F7673D /* Pods-DoughnutTests-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-DoughnutTests-acknowledgements.markdown"; sourceTree = "<group>"; }; + 2EA052A5ED7F6E02FB674087C3029623 /* SerializedDatabase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SerializedDatabase.swift; path = GRDB/Core/SerializedDatabase.swift; sourceTree = "<group>"; }; + 2FE2A6C2A6D0C8752CF6155EF89A9F32 /* FTS3.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FTS3.swift; path = GRDB/FTS/FTS3.swift; sourceTree = "<group>"; }; + 2FE55E91405466A2B73473046565A296 /* Configuration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Configuration.swift; path = GRDB/Core/Configuration.swift; sourceTree = "<group>"; }; + 2FFD7DF4FB94A15BB0B157D8A1D2E5FE /* RSSPath.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSPath.swift; path = Sources/FeedKit/Models/RSS/RSSPath.swift; sourceTree = "<group>"; }; + 345F727AEA9953BC160BCE131BEE6427 /* DatabaseValueConvertible+RawRepresentable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "DatabaseValueConvertible+RawRepresentable.swift"; path = "GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+RawRepresentable.swift"; sourceTree = "<group>"; }; + 3626575071AD091BD4BE691359B7C1A0 /* FeedDataType.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FeedDataType.swift; path = Sources/FeedKit/Parser/FeedDataType.swift; sourceTree = "<group>"; }; + 370626C546416F774136476700DE1038 /* NSData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NSData.swift; path = GRDB/Core/Support/Foundation/NSData.swift; sourceTree = "<group>"; }; + 38AAA3C7B1D07842DDC4CBE1D40E461B /* Pods-Doughnut.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Doughnut.debug.xcconfig"; sourceTree = "<group>"; }; + 39655D7D36C50E391F4E9D55F70711D0 /* Pods-DoughnutUITests-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-DoughnutUITests-resources.sh"; sourceTree = "<group>"; }; + 3AAF175727EB0D921FEE0B80A6BC1870 /* Fixits-0.101.1.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Fixits-0.101.1.swift"; path = "GRDB/Legacy/Fixits-0.101.1.swift"; sourceTree = "<group>"; }; + 3D69051CE87133DF7943048BB905865C /* DatabaseError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DatabaseError.swift; path = GRDB/Core/DatabaseError.swift; sourceTree = "<group>"; }; + 3DE2435666584AC8FB421A9493EE947E /* MASPreferences.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; path = MASPreferences.modulemap; sourceTree = "<group>"; }; + 3E015828D1204320A6E2A46DDD43A1A9 /* Pods-Doughnut-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-Doughnut-resources.sh"; sourceTree = "<group>"; }; + 3E65D049C763D17342C51AA684A137C6 /* MediaTag.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaTag.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaTag.swift; sourceTree = "<group>"; }; + 3F04F3CE523623FA28164FCDEBC53B4A /* MediaHash.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaHash.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaHash.swift; sourceTree = "<group>"; }; + 3F539FFB32290F4DFF61D1DD8198EFAE /* grdb_config.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = grdb_config.h; path = Support/grdb_config.h; sourceTree = "<group>"; }; + 3F91BDEB2D405F45EC2D361057B16F7D /* DatabaseValueConvertible+ReferenceConvertible.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "DatabaseValueConvertible+ReferenceConvertible.swift"; path = "GRDB/Core/Support/Foundation/DatabaseValueConvertible+ReferenceConvertible.swift"; sourceTree = "<group>"; }; + 402E1F03C653BC276A59FA62C0315ADA /* RSSFeedTextInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedTextInput.swift; path = Sources/FeedKit/Models/RSS/RSSFeedTextInput.swift; sourceTree = "<group>"; }; + 4085AE79FDBE5C4FDD656E2C3580CFF3 /* Migration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Migration.swift; path = GRDB/Migration/Migration.swift; sourceTree = "<group>"; }; + 4440A47EAF0EB92EBF82AD926AB818DF /* RFC3339DateFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RFC3339DateFormatter.swift; path = Sources/FeedKit/Dates/RFC3339DateFormatter.swift; sourceTree = "<group>"; }; + 4728069A0AA3F69C0DAA76DC03FE5AB8 /* Record.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Record.swift; path = GRDB/Record/Record.swift; sourceTree = "<group>"; }; + 477AAF4A448C0D60EBCFC3CCF0401DE5 /* Pods-DoughnutTests.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; path = "Pods-DoughnutTests.modulemap"; sourceTree = "<group>"; }; + 47945A421975422B7C38AFF86ABC741C /* Pods-DoughnutUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-DoughnutUITests.debug.xcconfig"; sourceTree = "<group>"; }; + 48933C4790981A5B9C41C01C95C37036 /* Request.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Request.swift; path = GRDB/Core/Request.swift; sourceTree = "<group>"; }; + 49187B7A0E9B59822C04AEAAF8806B3D /* Pods-DoughnutUITests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-DoughnutUITests-dummy.m"; sourceTree = "<group>"; }; + 4B9E910D1B4AE2A570378717CD26F6B0 /* FTS5.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FTS5.swift; path = GRDB/FTS/FTS5.swift; sourceTree = "<group>"; }; + 4BD4C79EE9270EF704FBA5014355AD6F /* MediaEmbed.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaEmbed.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaEmbed.swift; sourceTree = "<group>"; }; + 4E89F40015E13C8A15B2B97ABC2E0D2A /* FeedKit-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "FeedKit-prefix.pch"; sourceTree = "<group>"; }; + 4EBAD10A0F30CBA56EB0A8CDD93E800E /* GRDB.swift.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; path = GRDB.swift.modulemap; sourceTree = "<group>"; }; + 507BBD5E05AFFB3CCEC645B291402119 /* Fixits-Swift2.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Fixits-Swift2.swift"; path = "GRDB/Legacy/Fixits-Swift2.swift"; sourceTree = "<group>"; }; + 5248BD7939C5BDDD4886DF1588A999BF /* Fixits-0.102.0.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Fixits-0.102.0.swift"; path = "GRDB/Legacy/Fixits-0.102.0.swift"; sourceTree = "<group>"; }; + 52D381BB21B489004AC222F78F581533 /* Pods-DoughnutUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-DoughnutUITests.release.xcconfig"; sourceTree = "<group>"; }; + 5303BAC79922938BD120C6DC267F32C3 /* DatabaseValueConvertible+Decodable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "DatabaseValueConvertible+Decodable.swift"; path = "GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Decodable.swift"; sourceTree = "<group>"; }; + 5349F246A8B014B5AD1551E3BD7E4A2D /* SQLExpression+QueryInterface.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "SQLExpression+QueryInterface.swift"; path = "GRDB/QueryInterface/SQLExpression+QueryInterface.swift"; sourceTree = "<group>"; }; + 54735836C007714DAC3D8D4C506D3240 /* RDFPath.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RDFPath.swift; path = Sources/FeedKit/Models/RSS/RDFPath.swift; sourceTree = "<group>"; }; + 54EEC492649EF779C5583CF7582ADC60 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + 55B9D3DD609D42338F2E257E2BF9EAFD /* XMLFeedType.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = XMLFeedType.swift; path = Sources/FeedKit/Parser/XMLFeedType.swift; sourceTree = "<group>"; }; + 5617C9611D7032C96074A4E4B9E1DF3B /* FeedParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FeedParser.swift; path = Sources/FeedKit/Parser/FeedParser.swift; sourceTree = "<group>"; }; + 591897BEA10D71FACD9D3ED7EFF88424 /* SQLExpression.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SQLExpression.swift; path = GRDB/QueryInterface/SQLExpression.swift; sourceTree = "<group>"; }; + 59D92C57F5A2A0D7F7684294F61F384E /* MASPreferences.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = MASPreferences.xcconfig; sourceTree = "<group>"; }; + 5A209B680E8689CCB5FECC3B910BC1E1 /* Pods-DoughnutUITests-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-DoughnutUITests-frameworks.sh"; sourceTree = "<group>"; }; + 5A6CBF8B9643CF1E26A14496283E5DA4 /* MediaDescription.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaDescription.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaDescription.swift; sourceTree = "<group>"; }; + 5ABA0500029338AAB24476CD0AC60636 /* iTunesOwner.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = iTunesOwner.swift; path = Sources/FeedKit/Models/Namespaces/iTunes/iTunesOwner.swift; sourceTree = "<group>"; }; + 5AF263A347785618E1B42B7909071E59 /* AtomFeedEntryAuthor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedEntryAuthor.swift; path = Sources/FeedKit/Models/Atom/AtomFeedEntryAuthor.swift; sourceTree = "<group>"; }; + 5D34890E940700B6CBB4372925A5629E /* SQLSelectable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SQLSelectable.swift; path = GRDB/QueryInterface/SQLSelectable.swift; sourceTree = "<group>"; }; + 5DD14C34E748F3FFB1047DD6117B2B53 /* FeedKit.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = FeedKit.xcconfig; sourceTree = "<group>"; }; + 5DE1F6A45202874E735B0FA709AD787F /* MediaLicence.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaLicence.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaLicence.swift; sourceTree = "<group>"; }; + 5F570D72DB5F4A8CB79E6FDB1A647E42 /* Pods-Doughnut-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-Doughnut-umbrella.h"; sourceTree = "<group>"; }; + 62343700C604698F566303041B076C88 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + 626BFAFCFE80317D154CC9D0F3E04B44 /* RSSFeedItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedItem.swift; path = Sources/FeedKit/Models/RSS/RSSFeedItem.swift; sourceTree = "<group>"; }; + 62BFB7CBCB7EB2A55B3943699D8E30D8 /* DatabaseWriter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DatabaseWriter.swift; path = GRDB/Core/DatabaseWriter.swift; sourceTree = "<group>"; }; + 62E970FCB53AD5114A3ED81BB895758B /* TableMapping.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TableMapping.swift; path = GRDB/Record/TableMapping.swift; sourceTree = "<group>"; }; + 6389DA01E0ED3F7107896FF7AB2BB33E /* MediaCommunity.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaCommunity.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaCommunity.swift; sourceTree = "<group>"; }; + 6440B327F4D6A16BE250F1BACF5B2713 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + 65214CE868D1BDAE4E1AC38A8BC2C34E /* Array + Equatable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Array + Equatable.swift"; path = "Sources/FeedKit/Extensions/Array + Equatable.swift"; sourceTree = "<group>"; }; + 6526E38143AA3C7320583E142AA64538 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/System/Library/Frameworks/Cocoa.framework; sourceTree = DEVELOPER_DIR; }; + 655D5D910E7C1570B9ACB2326AAFD7B0 /* StandardLibrary.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = StandardLibrary.swift; path = GRDB/Core/Support/StandardLibrary/StandardLibrary.swift; sourceTree = "<group>"; }; + 66B654975C4A527AB75CCABCDE0A00DA /* FTS5TokenizerDescriptor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FTS5TokenizerDescriptor.swift; path = GRDB/FTS/FTS5TokenizerDescriptor.swift; sourceTree = "<group>"; }; + 69DEC08EF42EEC705FE3CFB01E22D920 /* RSSFeedSkipDay.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedSkipDay.swift; path = Sources/FeedKit/Models/RSS/RSSFeedSkipDay.swift; sourceTree = "<group>"; }; + 6EA26604087D6585F87D76A763B12361 /* Pods-DoughnutTests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-DoughnutTests-acknowledgements.plist"; sourceTree = "<group>"; }; + 6ED9EA722D4F1776976D4BFBEB0BBC60 /* Fixits-0.109.0.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Fixits-0.109.0.swift"; path = "GRDB/Legacy/Fixits-0.109.0.swift"; sourceTree = "<group>"; }; + 6EF86E191DEBE5C06D6D5B8DAA56325F /* FeedKit-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "FeedKit-dummy.m"; sourceTree = "<group>"; }; + 6F54ABE4B6F59407C9EAC318D1E30E33 /* SQLOrdering.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SQLOrdering.swift; path = GRDB/QueryInterface/SQLOrdering.swift; sourceTree = "<group>"; }; + 6FCF66BA1E5084A1E84E02F4DF36721C /* XMLFeedParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = XMLFeedParser.swift; path = Sources/FeedKit/Parser/XMLFeedParser.swift; sourceTree = "<group>"; }; + 70A3F74B9D0C5554C6464A91B04FC614 /* Pods-DoughnutUITests-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-DoughnutUITests-umbrella.h"; sourceTree = "<group>"; }; + 7124C06CE9E9F54136D812BEF58941A3 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + 756CD83642C176613222ED4F6407FA4E /* Persistable+Encodable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Persistable+Encodable.swift"; path = "GRDB/Record/Persistable+Encodable.swift"; sourceTree = "<group>"; }; + 78E9440E9227ED19949687BC0C4EC028 /* ISO8601DateFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ISO8601DateFormatter.swift; path = Sources/FeedKit/Dates/ISO8601DateFormatter.swift; sourceTree = "<group>"; }; + 799DB69FD68D658230A61F2D16A03C6F /* MediaThumbnail.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaThumbnail.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaThumbnail.swift; sourceTree = "<group>"; }; + 7B5E2EA9C94BAAD2DF20F479AD4B21DE /* Result.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Result.swift; path = GRDB/Utils/Result.swift; sourceTree = "<group>"; }; + 7F5AC5350AFA457332A5BE79141EDAB8 /* AtomPath.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomPath.swift; path = Sources/FeedKit/Models/Atom/AtomPath.swift; sourceTree = "<group>"; }; + 7FC743D751A0C8B97CBACB94F257B4D4 /* FeedKit.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; path = FeedKit.modulemap; sourceTree = "<group>"; }; + 808D59870017B81D7C4BB491E46E2FB5 /* DatabaseReader.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DatabaseReader.swift; path = GRDB/Core/DatabaseReader.swift; sourceTree = "<group>"; }; + 8347C6476E00478B07A624C646C6736D /* MediaStarRating.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaStarRating.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaStarRating.swift; sourceTree = "<group>"; }; + 839000271C33C4119287E2F824A15E93 /* ContentNamespace.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ContentNamespace.swift; path = Sources/FeedKit/Models/Namespaces/Content/ContentNamespace.swift; sourceTree = "<group>"; }; + 84A313CB04131D623C721B9524002E71 /* RSSFeedCloud.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedCloud.swift; path = Sources/FeedKit/Models/RSS/RSSFeedCloud.swift; sourceTree = "<group>"; }; + 8611935157B6EBA420E2C339F1C99B7D /* Fixits-0-84-0.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Fixits-0-84-0.swift"; path = "GRDB/Legacy/Fixits-0-84-0.swift"; sourceTree = "<group>"; }; + 8867DA4F4C846385BD5959EED30A9AE6 /* MASPreferences.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = MASPreferences.framework; path = MASPreferences.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 88F7654AEF8B4CFD06D6036EB8B40D28 /* RFC822DateFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RFC822DateFormatter.swift; path = Sources/FeedKit/Dates/RFC822DateFormatter.swift; sourceTree = "<group>"; }; + 89660E86ED40BAC0E1B18F677B38D934 /* DatabaseQueue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DatabaseQueue.swift; path = GRDB/Core/DatabaseQueue.swift; sourceTree = "<group>"; }; + 8993969726B6814C37AE798578562B77 /* MASPreferencesWindowController.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = MASPreferencesWindowController.h; path = Framework/MASPreferencesWindowController.h; sourceTree = "<group>"; }; + 8C2D1354EAEFF0B665ACC2ADFE3B9025 /* DatabaseValueConvertible+Encodable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "DatabaseValueConvertible+Encodable.swift"; path = "GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Encodable.swift"; sourceTree = "<group>"; }; + 8D0D878085ECFDC95DB94F95C38CEF33 /* DatabasePool.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DatabasePool.swift; path = GRDB/Core/DatabasePool.swift; sourceTree = "<group>"; }; + 8D4D46CD90FCC006A723CCDD0E73B115 /* Pods-DoughnutTests-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-DoughnutTests-frameworks.sh"; sourceTree = "<group>"; }; + 8E70452D49A78D4A4705F0BB5BB51363 /* SyndicationUpdatePeriod.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SyndicationUpdatePeriod.swift; path = Sources/FeedKit/Models/Namespaces/Syndication/SyndicationUpdatePeriod.swift; sourceTree = "<group>"; }; + 8F6539C6E6E8788E661DBE3294E4E05B /* AtomFeedEntryContributor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedEntryContributor.swift; path = Sources/FeedKit/Models/Atom/AtomFeedEntryContributor.swift; sourceTree = "<group>"; }; + 909D02D461734E0F0AB95898A4024D87 /* DatabaseDateComponents.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DatabaseDateComponents.swift; path = GRDB/Core/Support/Foundation/DatabaseDateComponents.swift; sourceTree = "<group>"; }; + 911F15D7F42F8B451A0AB8DCCBDED5FB /* Fixits-1.2.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Fixits-1.2.swift"; path = "GRDB/Legacy/Fixits-1.2.swift"; sourceTree = "<group>"; }; + 91C0BBC5E24A2CD358DB23464123FD09 /* Persistable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Persistable.swift; path = GRDB/Record/Persistable.swift; sourceTree = "<group>"; }; + 92949F3008F8A36A622F68FD7459C8E0 /* iTunesCategory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = iTunesCategory.swift; path = Sources/FeedKit/Models/Namespaces/iTunes/iTunesCategory.swift; sourceTree = "<group>"; }; + 93A4A3777CF96A4AAC1D13BA6DCCEA73 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + 942965EB959AD61EFD82EA92F993292C /* SQLSpecificExpressible+QueryInterface.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "SQLSpecificExpressible+QueryInterface.swift"; path = "GRDB/QueryInterface/SQLSpecificExpressible+QueryInterface.swift"; sourceTree = "<group>"; }; + 9753E560EAA431257EA6A208A6189F1C /* Database.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Database.swift; path = GRDB/Core/Database.swift; sourceTree = "<group>"; }; + 97B6DC785F4AF05D64662BF84DF8A4C4 /* CGFloat.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CGFloat.swift; path = GRDB/Core/Support/CoreGraphics/CGFloat.swift; sourceTree = "<group>"; }; + 97BEAA6413FFCB46AC6B51770FE8C168 /* QueryInterfaceSelectQueryDefinition.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = QueryInterfaceSelectQueryDefinition.swift; path = GRDB/QueryInterface/QueryInterfaceSelectQueryDefinition.swift; sourceTree = "<group>"; }; + 988EE2ACD7FF2D0A5D8B413950E68C3C /* AtomFeedAuthor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedAuthor.swift; path = Sources/FeedKit/Models/Atom/AtomFeedAuthor.swift; sourceTree = "<group>"; }; + 98A2208AB57BE1FEBE9255D832D7D7B4 /* AtomFeedEntrySource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedEntrySource.swift; path = Sources/FeedKit/Models/Atom/AtomFeedEntrySource.swift; sourceTree = "<group>"; }; + 990043F5B82A0419C8681BFC6DCF3BA5 /* DatabaseValue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DatabaseValue.swift; path = GRDB/Core/DatabaseValue.swift; sourceTree = "<group>"; }; + 9ACD26AA6790B08049C2C449EA23CA33 /* MediaGroup.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaGroup.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaGroup.swift; sourceTree = "<group>"; }; + 9AE812A0D85492CDB53E68C143162F0B /* Pods_DoughnutUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_DoughnutUITests.framework; path = "Pods-DoughnutUITests.framework"; sourceTree = BUILT_PRODUCTS_DIR; }; + 9BC0D57CC36E24162EF8B0B7BF77D7CD /* FeedKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = FeedKit.framework; path = FeedKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9BE3504411BBEE0B3855BA65FC602342 /* AtomFeedEntryCategory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedEntryCategory.swift; path = Sources/FeedKit/Models/Atom/AtomFeedEntryCategory.swift; sourceTree = "<group>"; }; + 9C10A891374D7ACA5D0AEF3D007FFDA3 /* GRDB.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = GRDB.framework; path = GRDB.swift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9D739B440A5ABF34D83780606966390A /* FTS4.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FTS4.swift; path = GRDB/FTS/FTS4.swift; sourceTree = "<group>"; }; + 9D933BADB401D42C579D4BB3E65E9F6B /* Pods_DoughnutTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_DoughnutTests.framework; path = "Pods-DoughnutTests.framework"; sourceTree = BUILT_PRODUCTS_DIR; }; + 9DCAC7083C7CF2E9D85BB9C7600139F2 /* ReadWriteBox.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ReadWriteBox.swift; path = GRDB/Utils/ReadWriteBox.swift; sourceTree = "<group>"; }; + A23056F7698CAD7C802996DF49FC9F6E /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + A375A121C95AE6A31CEE5765B1F812CF /* RSSFeedItemCategory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedItemCategory.swift; path = Sources/FeedKit/Models/RSS/RSSFeedItemCategory.swift; sourceTree = "<group>"; }; + A62F2BADE8667533499FF2633F916702 /* SQLCollection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SQLCollection.swift; path = GRDB/QueryInterface/SQLCollection.swift; sourceTree = "<group>"; }; + A744578CEA7FCC611331AC7A16ACA2AB /* VirtualTableModule.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = VirtualTableModule.swift; path = GRDB/QueryInterface/VirtualTableModule.swift; sourceTree = "<group>"; }; + A747BFEE7725184A738A4AD8214B33EA /* MediaCredit.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaCredit.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaCredit.swift; sourceTree = "<group>"; }; + A844E23A4174EE027F5829C70BE84605 /* JSONFeedAttachment.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = JSONFeedAttachment.swift; path = Sources/FeedKit/Models/JSON/JSONFeedAttachment.swift; sourceTree = "<group>"; }; + A8589E48767597581056294CB938B043 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + A8876D2FE66CCB91AD8DEF3597EB66A7 /* Result.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Result.swift; path = Sources/FeedKit/Parser/Result.swift; sourceTree = "<group>"; }; + AA830A16FECE6E7A873C6709E18C12C8 /* Data.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Data.swift; path = GRDB/Core/Support/Foundation/Data.swift; sourceTree = "<group>"; }; + AC666F41DE4D65527C1C6C54BBC061DA /* MASPreferencesViewController.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = MASPreferencesViewController.h; path = Framework/MASPreferencesViewController.h; sourceTree = "<group>"; }; + AC920E72A2A8627ECFF1E5A2CFAD186B /* AtomFeedSubtitle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedSubtitle.swift; path = Sources/FeedKit/Models/Atom/AtomFeedSubtitle.swift; sourceTree = "<group>"; }; + ACFB5649B721298AFD991FE3B9B45887 /* StatementColumnConvertible.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = StatementColumnConvertible.swift; path = GRDB/Core/StatementColumnConvertible.swift; sourceTree = "<group>"; }; + ADA324DD169521C25B580FBB406FB339 /* MediaCopyright.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaCopyright.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaCopyright.swift; sourceTree = "<group>"; }; + AEC4BD3E87F3F9A8ED67476718C8BE6C /* SQLOperators.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SQLOperators.swift; path = GRDB/QueryInterface/Support/SQLOperators.swift; sourceTree = "<group>"; }; + B09ADF172A609B9FDC957B61799FE018 /* RSSFeed + mapAttributes.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "RSSFeed + mapAttributes.swift"; path = "Sources/FeedKit/Models/RSS/RSSFeed + mapAttributes.swift"; sourceTree = "<group>"; }; + B141710B0BCDE875436149CB37450704 /* String + toDuration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "String + toDuration.swift"; path = "Sources/FeedKit/Extensions/String + toDuration.swift"; sourceTree = "<group>"; }; + B194E721E4940F89173AFEF1A6DD3474 /* JSONFeedAuthor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = JSONFeedAuthor.swift; path = Sources/FeedKit/Models/JSON/JSONFeedAuthor.swift; sourceTree = "<group>"; }; + B1B3E456071D498DCDDFB074B684C03E /* Statement.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Statement.swift; path = GRDB/Core/Statement.swift; sourceTree = "<group>"; }; + B1F5D47B7D940D112D101AC6252E1F39 /* MediaPlayer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaPlayer.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaPlayer.swift; sourceTree = "<group>"; }; + B37B3E47779172C35E55ACF1B5189D4C /* Pods-DoughnutUITests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-DoughnutUITests-acknowledgements.plist"; sourceTree = "<group>"; }; + B4788A1A34E8963E736947E400DE9CC8 /* Pods-Doughnut-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-Doughnut-acknowledgements.markdown"; sourceTree = "<group>"; }; + B4A2B52EFE7333221F7CF81012AC5F67 /* JSONFeedItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = JSONFeedItem.swift; path = Sources/FeedKit/Models/JSON/JSONFeedItem.swift; sourceTree = "<group>"; }; + B4B353141BFC1CCF8446F81E09DE99C8 /* SQLSelectable+QueryInterface.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "SQLSelectable+QueryInterface.swift"; path = "GRDB/QueryInterface/SQLSelectable+QueryInterface.swift"; sourceTree = "<group>"; }; + B578A434E856D11E6137DB9016572807 /* Pods-Doughnut.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Doughnut.release.xcconfig"; sourceTree = "<group>"; }; + B76057D84DFD5E2851EC4FE792E0EE6B /* iTunesImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = iTunesImage.swift; path = Sources/FeedKit/Models/Namespaces/iTunes/iTunesImage.swift; sourceTree = "<group>"; }; + B8C5F30E5A3ABF44784B7B18B9CB38EC /* FeedParserProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FeedParserProtocol.swift; path = Sources/FeedKit/Parser/FeedParserProtocol.swift; sourceTree = "<group>"; }; + B9981D05528B0F8A77DBDB8D1C39AC2C /* Fixits-0-90-1.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Fixits-0-90-1.swift"; path = "GRDB/Legacy/Fixits-0-90-1.swift"; sourceTree = "<group>"; }; + B9A4B8183704A49BA77BC4E0E093D009 /* JSONFeedParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = JSONFeedParser.swift; path = Sources/FeedKit/Parser/JSONFeedParser.swift; sourceTree = "<group>"; }; + B9A97FF1AD255831C3D35EF762EC3963 /* MASPreferences-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "MASPreferences-umbrella.h"; sourceTree = "<group>"; }; + BA3F9E0DFFA40C92040777A1E1F11F4C /* Pods-Doughnut-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-Doughnut-acknowledgements.plist"; sourceTree = "<group>"; }; + BACAABB13C2049B29778580D51A8B921 /* RowConvertible+TableMapping.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "RowConvertible+TableMapping.swift"; path = "GRDB/Record/RowConvertible+TableMapping.swift"; sourceTree = "<group>"; }; + BB9041535FA1AA6F1A1A143569E15235 /* Pods-DoughnutUITests-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-DoughnutUITests-acknowledgements.markdown"; sourceTree = "<group>"; }; + BBB082807ACF8CF6B11D0B2FC78F324A /* AtomFeedCategory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedCategory.swift; path = Sources/FeedKit/Models/Atom/AtomFeedCategory.swift; sourceTree = "<group>"; }; + BEB0649B2A1A987642397C1C4E8D6133 /* Pods-Doughnut-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-Doughnut-dummy.m"; sourceTree = "<group>"; }; + BED223170225B1433D32CDB60E49296E /* AtomFeedEntryLink.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedEntryLink.swift; path = Sources/FeedKit/Models/Atom/AtomFeedEntryLink.swift; sourceTree = "<group>"; }; + BEFAB4A5D221AC58521B7FA1C9A28DE7 /* AtomFeedLink.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedLink.swift; path = Sources/FeedKit/Models/Atom/AtomFeedLink.swift; sourceTree = "<group>"; }; + BF9F3C41B84EDF9348354733659FD0FD /* MediaParam.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaParam.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaParam.swift; sourceTree = "<group>"; }; + BFCCD4563D2890C2F24004E30E87F652 /* DatabaseFunction.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DatabaseFunction.swift; path = GRDB/Core/DatabaseFunction.swift; sourceTree = "<group>"; }; + C0FD700528DE5AC13168918C20F51190 /* GRDB.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GRDB.h; path = Support/GRDB.h; sourceTree = "<group>"; }; + C2708E731D7937F85464E817C4F1F392 /* AtomFeedEntry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedEntry.swift; path = Sources/FeedKit/Models/Atom/AtomFeedEntry.swift; sourceTree = "<group>"; }; + C2FCAEA387C94AF0A27D0AB48220ED2C /* Pods-Doughnut.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; path = "Pods-Doughnut.modulemap"; sourceTree = "<group>"; }; + C477BBD3E1538F96AEE26B08AAEBA830 /* GRDB.swift-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "GRDB.swift-prefix.pch"; sourceTree = "<group>"; }; + C5B6579C6525893DF3C918F55CA5100B /* FTS5Pattern.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FTS5Pattern.swift; path = GRDB/FTS/FTS5Pattern.swift; sourceTree = "<group>"; }; + C6868B56A9CF1EA8B774C19F1E312FB0 /* RSSFeedSkipHour.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedSkipHour.swift; path = Sources/FeedKit/Models/RSS/RSSFeedSkipHour.swift; sourceTree = "<group>"; }; + C6EBBD1E2BB17FED1D25D9FFBA4AE8DA /* iTunesNamespace.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = iTunesNamespace.swift; path = Sources/FeedKit/Models/Namespaces/iTunes/iTunesNamespace.swift; sourceTree = "<group>"; }; + C7FAE255C5D7FEC3964E2F4B82430383 /* Pods-DoughnutTests-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-DoughnutTests-resources.sh"; sourceTree = "<group>"; }; + C8A49C0DDB44E19ED62DD5DAED3C37B1 /* Row.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Row.swift; path = GRDB/Core/Row.swift; sourceTree = "<group>"; }; + C8FADD9E1FC21D7D047AC33F719E1EB2 /* AtomFeed + mapCharacters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "AtomFeed + mapCharacters.swift"; path = "Sources/FeedKit/Models/Atom/AtomFeed + mapCharacters.swift"; sourceTree = "<group>"; }; + C9878D2CA813DFF2A735ED27855C6F95 /* UUID.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = UUID.swift; path = GRDB/Core/Support/Foundation/UUID.swift; sourceTree = "<group>"; }; + CB0E864CE99D47F0FFC7C0A97D12D26C /* Date.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Date.swift; path = GRDB/Core/Support/Foundation/Date.swift; sourceTree = "<group>"; }; + CB717BC79D7B38246221EFE3E43E0FAB /* SQLCollatedExpression.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SQLCollatedExpression.swift; path = GRDB/QueryInterface/SQLCollatedExpression.swift; sourceTree = "<group>"; }; + CC2C9ADD34D3F9F4235FA3C4EEA8919B /* Pods-Doughnut-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-Doughnut-frameworks.sh"; sourceTree = "<group>"; }; + CCAD435CDE8E1DA04D186B5C30D21ADF /* FTS3TokenizerDescriptor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FTS3TokenizerDescriptor.swift; path = GRDB/FTS/FTS3TokenizerDescriptor.swift; sourceTree = "<group>"; }; + D0099E7DEAC48069E5DCCF8235795E55 /* JSONFeedHub.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = JSONFeedHub.swift; path = Sources/FeedKit/Models/JSON/JSONFeedHub.swift; sourceTree = "<group>"; }; + D0147059819728AFACFC2E4BE809B235 /* RowConvertible+Decodable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "RowConvertible+Decodable.swift"; path = "GRDB/Record/RowConvertible+Decodable.swift"; sourceTree = "<group>"; }; + D1556B2332BE75D499CF0A40BE0C1966 /* SyndicationNamespace.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SyndicationNamespace.swift; path = Sources/FeedKit/Models/Namespaces/Syndication/SyndicationNamespace.swift; sourceTree = "<group>"; }; + D328195E55287400DD1F7E05DEFDDF2F /* RSSFeedCategory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedCategory.swift; path = Sources/FeedKit/Models/RSS/RSSFeedCategory.swift; sourceTree = "<group>"; }; + D3ABD05CDD021FE5537B23CEFF951FDC /* SQLExpressible.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SQLExpressible.swift; path = GRDB/QueryInterface/SQLExpressible.swift; sourceTree = "<group>"; }; + D3D9FFD14F2949364162F5568B74093E /* RowAdapter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RowAdapter.swift; path = GRDB/Core/RowAdapter.swift; sourceTree = "<group>"; }; + D5E3FA503DE3C156EE18759FCBBCB92B /* NSString.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NSString.swift; path = GRDB/Core/Support/Foundation/NSString.swift; sourceTree = "<group>"; }; + D83DE56D56DF7F33A58CDE3FBE5C74BC /* DatabaseMigrator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DatabaseMigrator.swift; path = GRDB/Migration/DatabaseMigrator.swift; sourceTree = "<group>"; }; + D970DD209D94BFB0FF1104FE93E63CE3 /* FTS5+QueryInterface.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "FTS5+QueryInterface.swift"; path = "GRDB/QueryInterface/FTS5+QueryInterface.swift"; sourceTree = "<group>"; }; + D98F0F0252FBD4913F0BB83FADF61803 /* AtomFeed + mapAttributes.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "AtomFeed + mapAttributes.swift"; path = "Sources/FeedKit/Models/Atom/AtomFeed + mapAttributes.swift"; sourceTree = "<group>"; }; + DA9A219DAC909D640A057B61485EAA4C /* iTunesSubCategory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = iTunesSubCategory.swift; path = Sources/FeedKit/Models/Namespaces/iTunes/iTunesSubCategory.swift; sourceTree = "<group>"; }; + DC234BBB7B09EBF0829EFC3CCB7F5D34 /* MediaRights.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaRights.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaRights.swift; sourceTree = "<group>"; }; + DD282198E99B8CEB3193D8386D4569E0 /* RSSFeedItemSource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedItemSource.swift; path = Sources/FeedKit/Models/RSS/RSSFeedItemSource.swift; sourceTree = "<group>"; }; + DD6081E2379FA9B56747171B94180401 /* SchedulingWatchdog.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SchedulingWatchdog.swift; path = GRDB/Core/SchedulingWatchdog.swift; sourceTree = "<group>"; }; + DEFFD25D2E91CE72BB3A50FC6DDC958D /* Pods-DoughnutTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-DoughnutTests.release.xcconfig"; sourceTree = "<group>"; }; + E1B9AFD80AED75987D51508DF02F75D4 /* String + toDate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "String + toDate.swift"; path = "Sources/FeedKit/Extensions/String + toDate.swift"; sourceTree = "<group>"; }; + E1D0ED7D249D7FF63E2F970EDCD563F0 /* Data + toUtf8.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Data + toUtf8.swift"; path = "Sources/FeedKit/Extensions/Data + toUtf8.swift"; sourceTree = "<group>"; }; + E5A83F6F7498D2CDF5B190A4F96CCB46 /* Pods-DoughnutTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-DoughnutTests.debug.xcconfig"; sourceTree = "<group>"; }; + E763DAE78E191A7E48589E1A4709B76B /* MASPreferencesWindowController.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = MASPreferencesWindowController.m; path = Framework/MASPreferencesWindowController.m; sourceTree = "<group>"; }; + EA366C8B2AE0983FCFF8D03E243FA291 /* Pods_Doughnut.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_Doughnut.framework; path = "Pods-Doughnut.framework"; sourceTree = BUILT_PRODUCTS_DIR; }; + EBC9A165C781CD0AAA4E579881930270 /* QueryInterfaceRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = QueryInterfaceRequest.swift; path = GRDB/QueryInterface/QueryInterfaceRequest.swift; sourceTree = "<group>"; }; + EBD5047055F2913D7879954318068002 /* MASPreferences-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "MASPreferences-prefix.pch"; sourceTree = "<group>"; }; + EEA945B026139344D59E2C046CA046BE /* MASPreferences.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = MASPreferences.h; path = Framework/MASPreferences.h; sourceTree = "<group>"; }; + EF1428D9482CFAAF26CF31E67ACB2BB3 /* RSSFeed + mapCharacters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "RSSFeed + mapCharacters.swift"; path = "Sources/FeedKit/Models/RSS/RSSFeed + mapCharacters.swift"; sourceTree = "<group>"; }; + EF2BBC2A5306F8C330A6FC3FE9F335D7 /* Pods-DoughnutTests.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-DoughnutTests.test.xcconfig"; sourceTree = "<group>"; }; + EF5181AF1444B08132A17637E28D5255 /* FTS5WrapperTokenizer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FTS5WrapperTokenizer.swift; path = GRDB/FTS/FTS5WrapperTokenizer.swift; sourceTree = "<group>"; }; + EFFBF27EB0F493F55534CDAA47439E6F /* DatabaseValueConvertible.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DatabaseValueConvertible.swift; path = GRDB/Core/DatabaseValueConvertible.swift; sourceTree = "<group>"; }; + EFFD3F77AA195135E9815DB402EE79BC /* FeedKit-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "FeedKit-umbrella.h"; sourceTree = "<group>"; }; + F10F7E72A3C0476B5CBE7BCD76AEB544 /* DatabaseSchemaCache.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DatabaseSchemaCache.swift; path = GRDB/Core/DatabaseSchemaCache.swift; sourceTree = "<group>"; }; + F16B07D6738B2921FC72E05C59FBA928 /* FTS3+QueryInterfaceRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "FTS3+QueryInterfaceRequest.swift"; path = "GRDB/QueryInterface/FTS3+QueryInterfaceRequest.swift"; sourceTree = "<group>"; }; + F3940F027FB8051D776545A5FAFEE14E /* AtomFeedEntryContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedEntryContent.swift; path = Sources/FeedKit/Models/Atom/AtomFeedEntryContent.swift; sourceTree = "<group>"; }; + F43BD907312A2BC39FAE7B25ACF4E8B4 /* AtomFeedContributor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedContributor.swift; path = Sources/FeedKit/Models/Atom/AtomFeedContributor.swift; sourceTree = "<group>"; }; + F6EFBAB00C4728232E692083A01126BE /* TableDefinition.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TableDefinition.swift; path = GRDB/QueryInterface/TableDefinition.swift; sourceTree = "<group>"; }; + FAA76253F7D1E4B224B809F4D43D79D0 /* MediaPrice.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaPrice.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaPrice.swift; sourceTree = "<group>"; }; + FAE2192D7F604E57CDD62100745633A5 /* ParserError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ParserError.swift; path = Sources/FeedKit/Parser/ParserError.swift; sourceTree = "<group>"; }; + FB282CFF6640F828AC6C371DD1F161D4 /* Pool.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Pool.swift; path = GRDB/Utils/Pool.swift; sourceTree = "<group>"; }; + FB7947ADE87755EFDB2B745A668402C7 /* GRDB.swift.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = GRDB.swift.xcconfig; sourceTree = "<group>"; }; + FDAD4E054102D819310D5AAA60A3A0C3 /* RSSFeedItemEnclosure.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedItemEnclosure.swift; path = Sources/FeedKit/Models/RSS/RSSFeedItemEnclosure.swift; sourceTree = "<group>"; }; + FF403126D6AD8F4F71602596FB86C4B7 /* FTS5Tokenizer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FTS5Tokenizer.swift; path = GRDB/FTS/FTS5Tokenizer.swift; sourceTree = "<group>"; }; + FFEF126CA2ABB0DF935BA3160D49EA1C /* RSSFeedImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedImage.swift; path = Sources/FeedKit/Models/RSS/RSSFeedImage.swift; sourceTree = "<group>"; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3BB15557A7688764EAEF2AB2771071BB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ADA3B0939DA8061C5DB29BF6F3A335B4 /* Cocoa.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4C8C3CC1191C5C1B62A29278AA69BED0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AACD2D2061D7058C56DB176C5A814393 /* Cocoa.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 58D77401A8A48676FCA2C5BB50AA9342 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B770C4BE33B1B0F277B1E230D5DD21B1 /* Cocoa.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A360AFF726A03C175427B0B89961E57A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 84A262BF780BF7D1E23F54AFF778D406 /* Cocoa.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A7BA9AD4C3E97C2C60E90AD605DDE795 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 945F7D9D42E99827AE3020683AC7D4A9 /* Cocoa.framework in Frameworks */, + 036717BC5600473E188068146F5D7293 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F6DBAE7AF45696FFFFF66D1E304754A7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DA1C4F13F6B316318E91CC19B9AEA5DE /* Cocoa.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3AE4BEFEA5F95234EDB381342F9FECFF /* Products */ = { + isa = PBXGroup; + children = ( + 9BC0D57CC36E24162EF8B0B7BF77D7CD /* FeedKit.framework */, + 9C10A891374D7ACA5D0AEF3D007FFDA3 /* GRDB.framework */, + 8867DA4F4C846385BD5959EED30A9AE6 /* MASPreferences.framework */, + EA366C8B2AE0983FCFF8D03E243FA291 /* Pods_Doughnut.framework */, + 9D933BADB401D42C579D4BB3E65E9F6B /* Pods_DoughnutTests.framework */, + 9AE812A0D85492CDB53E68C143162F0B /* Pods_DoughnutUITests.framework */, + ); + name = Products; + sourceTree = "<group>"; + }; + 5FA58EECA2986FD8C41A97A90CEF787D /* MASPreferences */ = { + isa = PBXGroup; + children = ( + EEA945B026139344D59E2C046CA046BE /* MASPreferences.h */, + AC666F41DE4D65527C1C6C54BBC061DA /* MASPreferencesViewController.h */, + 8993969726B6814C37AE798578562B77 /* MASPreferencesWindowController.h */, + E763DAE78E191A7E48589E1A4709B76B /* MASPreferencesWindowController.m */, + FF055A3110D449AA3D57409B2D6DE2D0 /* Resources */, + 81EEC749343A32D9AABD3D43A5E3432E /* Support Files */, + ); + name = MASPreferences; + path = MASPreferences; + sourceTree = "<group>"; + }; + 62640CA783BEA40D4B2E538D5A57F911 /* FeedKit */ = { + isa = PBXGroup; + children = ( + 65214CE868D1BDAE4E1AC38A8BC2C34E /* Array + Equatable.swift */, + 0F0411FDC4E4B8F16B23018CDE02F545 /* AtomFeed.swift */, + D98F0F0252FBD4913F0BB83FADF61803 /* AtomFeed + mapAttributes.swift */, + C8FADD9E1FC21D7D047AC33F719E1EB2 /* AtomFeed + mapCharacters.swift */, + 988EE2ACD7FF2D0A5D8B413950E68C3C /* AtomFeedAuthor.swift */, + BBB082807ACF8CF6B11D0B2FC78F324A /* AtomFeedCategory.swift */, + F43BD907312A2BC39FAE7B25ACF4E8B4 /* AtomFeedContributor.swift */, + C2708E731D7937F85464E817C4F1F392 /* AtomFeedEntry.swift */, + 5AF263A347785618E1B42B7909071E59 /* AtomFeedEntryAuthor.swift */, + 9BE3504411BBEE0B3855BA65FC602342 /* AtomFeedEntryCategory.swift */, + F3940F027FB8051D776545A5FAFEE14E /* AtomFeedEntryContent.swift */, + 8F6539C6E6E8788E661DBE3294E4E05B /* AtomFeedEntryContributor.swift */, + BED223170225B1433D32CDB60E49296E /* AtomFeedEntryLink.swift */, + 98A2208AB57BE1FEBE9255D832D7D7B4 /* AtomFeedEntrySource.swift */, + 092B20E7ED758074C1F57B5E8C2CAA7F /* AtomFeedEntrySummary.swift */, + 273F49BE94FFE15CC030E0A6367D7452 /* AtomFeedGenerator.swift */, + BEFAB4A5D221AC58521B7FA1C9A28DE7 /* AtomFeedLink.swift */, + AC920E72A2A8627ECFF1E5A2CFAD186B /* AtomFeedSubtitle.swift */, + 7F5AC5350AFA457332A5BE79141EDAB8 /* AtomPath.swift */, + 839000271C33C4119287E2F824A15E93 /* ContentNamespace.swift */, + E1D0ED7D249D7FF63E2F970EDCD563F0 /* Data + toUtf8.swift */, + 1BC625877D66709B8ADE6A7540DF37FD /* DateSpec.swift */, + 17509CB56392BAE025F2FBB08DA5C564 /* DublinCoreNamespace.swift */, + 3626575071AD091BD4BE691359B7C1A0 /* FeedDataType.swift */, + 5617C9611D7032C96074A4E4B9E1DF3B /* FeedParser.swift */, + B8C5F30E5A3ABF44784B7B18B9CB38EC /* FeedParserProtocol.swift */, + 78E9440E9227ED19949687BC0C4EC028 /* ISO8601DateFormatter.swift */, + 92949F3008F8A36A622F68FD7459C8E0 /* iTunesCategory.swift */, + B76057D84DFD5E2851EC4FE792E0EE6B /* iTunesImage.swift */, + C6EBBD1E2BB17FED1D25D9FFBA4AE8DA /* iTunesNamespace.swift */, + 5ABA0500029338AAB24476CD0AC60636 /* iTunesOwner.swift */, + DA9A219DAC909D640A057B61485EAA4C /* iTunesSubCategory.swift */, + 06BAE4D76B6147E2349A91CA1B281605 /* JSONFeed.swift */, + A844E23A4174EE027F5829C70BE84605 /* JSONFeedAttachment.swift */, + B194E721E4940F89173AFEF1A6DD3474 /* JSONFeedAuthor.swift */, + D0099E7DEAC48069E5DCCF8235795E55 /* JSONFeedHub.swift */, + B4A2B52EFE7333221F7CF81012AC5F67 /* JSONFeedItem.swift */, + B9A4B8183704A49BA77BC4E0E093D009 /* JSONFeedParser.swift */, + 18A370ECF8E9E52E231E21BD8C255242 /* MediaCategory.swift */, + 6389DA01E0ED3F7107896FF7AB2BB33E /* MediaCommunity.swift */, + 0E72CA31472884A973CA9402D7A4B823 /* MediaContent.swift */, + ADA324DD169521C25B580FBB406FB339 /* MediaCopyright.swift */, + A747BFEE7725184A738A4AD8214B33EA /* MediaCredit.swift */, + 5A6CBF8B9643CF1E26A14496283E5DA4 /* MediaDescription.swift */, + 4BD4C79EE9270EF704FBA5014355AD6F /* MediaEmbed.swift */, + 9ACD26AA6790B08049C2C449EA23CA33 /* MediaGroup.swift */, + 3F04F3CE523623FA28164FCDEBC53B4A /* MediaHash.swift */, + 5DE1F6A45202874E735B0FA709AD787F /* MediaLicence.swift */, + 11C0C15882F9128409587BC60986CAE6 /* MediaLocation.swift */, + 2BCD90EFCB2BF6BC146A94E64F28B7CB /* MediaNamespace.swift */, + BF9F3C41B84EDF9348354733659FD0FD /* MediaParam.swift */, + 1E5C3097E6A6E483EDDB3AF1AC441CE2 /* MediaPeerLink.swift */, + B1F5D47B7D940D112D101AC6252E1F39 /* MediaPlayer.swift */, + FAA76253F7D1E4B224B809F4D43D79D0 /* MediaPrice.swift */, + 01647BDBF7E7728E44DE1290E9D7331C /* MediaRating.swift */, + 06777E6CFC608C7E445AD4CF0DFAA689 /* MediaRestriction.swift */, + DC234BBB7B09EBF0829EFC3CCB7F5D34 /* MediaRights.swift */, + 15B317C4BA95971FEB55DFD99FD62B88 /* MediaScene.swift */, + 8347C6476E00478B07A624C646C6736D /* MediaStarRating.swift */, + 1FF3615C4D8B01BC8825F8C215A10CB7 /* MediaStatistics.swift */, + 26945C000AA9D7D348898F53C3F26C6B /* MediaStatus.swift */, + 00E9A76B73910290BD0454CAF82739C9 /* MediaSubTitle.swift */, + 3E65D049C763D17342C51AA684A137C6 /* MediaTag.swift */, + 123366D60146A2DE2CDCC1D9989E3588 /* MediaText.swift */, + 799DB69FD68D658230A61F2D16A03C6F /* MediaThumbnail.swift */, + 077BBF7D76B836ADD3E6C522FD4E0CC6 /* MediaTitle.swift */, + FAE2192D7F604E57CDD62100745633A5 /* ParserError.swift */, + 54735836C007714DAC3D8D4C506D3240 /* RDFPath.swift */, + A8876D2FE66CCB91AD8DEF3597EB66A7 /* Result.swift */, + 4440A47EAF0EB92EBF82AD926AB818DF /* RFC3339DateFormatter.swift */, + 88F7654AEF8B4CFD06D6036EB8B40D28 /* RFC822DateFormatter.swift */, + 2090385CD59D9E249D286138C448874D /* RSSFeed.swift */, + B09ADF172A609B9FDC957B61799FE018 /* RSSFeed + mapAttributes.swift */, + EF1428D9482CFAAF26CF31E67ACB2BB3 /* RSSFeed + mapCharacters.swift */, + D328195E55287400DD1F7E05DEFDDF2F /* RSSFeedCategory.swift */, + 84A313CB04131D623C721B9524002E71 /* RSSFeedCloud.swift */, + FFEF126CA2ABB0DF935BA3160D49EA1C /* RSSFeedImage.swift */, + 626BFAFCFE80317D154CC9D0F3E04B44 /* RSSFeedItem.swift */, + A375A121C95AE6A31CEE5765B1F812CF /* RSSFeedItemCategory.swift */, + FDAD4E054102D819310D5AAA60A3A0C3 /* RSSFeedItemEnclosure.swift */, + 155D2F785AC51B9BE3285E8D588D263F /* RSSFeedItemGUID.swift */, + DD282198E99B8CEB3193D8386D4569E0 /* RSSFeedItemSource.swift */, + 69DEC08EF42EEC705FE3CFB01E22D920 /* RSSFeedSkipDay.swift */, + C6868B56A9CF1EA8B774C19F1E312FB0 /* RSSFeedSkipHour.swift */, + 402E1F03C653BC276A59FA62C0315ADA /* RSSFeedTextInput.swift */, + 2FFD7DF4FB94A15BB0B157D8A1D2E5FE /* RSSPath.swift */, + 0BDC0D2CCBE0F8F1A76E5D16D4768348 /* String + toBool.swift */, + E1B9AFD80AED75987D51508DF02F75D4 /* String + toDate.swift */, + B141710B0BCDE875436149CB37450704 /* String + toDuration.swift */, + D1556B2332BE75D499CF0A40BE0C1966 /* SyndicationNamespace.swift */, + 8E70452D49A78D4A4705F0BB5BB51363 /* SyndicationUpdatePeriod.swift */, + 6FCF66BA1E5084A1E84E02F4DF36721C /* XMLFeedParser.swift */, + 55B9D3DD609D42338F2E257E2BF9EAFD /* XMLFeedType.swift */, + C1B00AD157485328ADFA6CA523A4A36C /* Support Files */, + ); + name = FeedKit; + path = FeedKit; + sourceTree = "<group>"; + }; + 634D72C5571DCE0BE31053FFE4A19CAF /* Pods-DoughnutUITests */ = { + isa = PBXGroup; + children = ( + 7124C06CE9E9F54136D812BEF58941A3 /* Info.plist */, + 19EEA5C8D80464C5B246C7701C942623 /* Pods-DoughnutUITests.modulemap */, + BB9041535FA1AA6F1A1A143569E15235 /* Pods-DoughnutUITests-acknowledgements.markdown */, + B37B3E47779172C35E55ACF1B5189D4C /* Pods-DoughnutUITests-acknowledgements.plist */, + 49187B7A0E9B59822C04AEAAF8806B3D /* Pods-DoughnutUITests-dummy.m */, + 5A209B680E8689CCB5FECC3B910BC1E1 /* Pods-DoughnutUITests-frameworks.sh */, + 39655D7D36C50E391F4E9D55F70711D0 /* Pods-DoughnutUITests-resources.sh */, + 70A3F74B9D0C5554C6464A91B04FC614 /* Pods-DoughnutUITests-umbrella.h */, + 47945A421975422B7C38AFF86ABC741C /* Pods-DoughnutUITests.debug.xcconfig */, + 52D381BB21B489004AC222F78F581533 /* Pods-DoughnutUITests.release.xcconfig */, + 0202316A0F9E2D5123231CC9B185FA48 /* Pods-DoughnutUITests.test.xcconfig */, + ); + name = "Pods-DoughnutUITests"; + path = "Target Support Files/Pods-DoughnutUITests"; + sourceTree = "<group>"; + }; + 7DB346D0F39D3F0E887471402A8071AB = { + isa = PBXGroup; + children = ( + 93A4A3777CF96A4AAC1D13BA6DCCEA73 /* Podfile */, + D648CE86F139C7CCFD55D5B8A03BE74B /* Frameworks */, + AE260A7F851B32589482D6FC96873BC8 /* Pods */, + 3AE4BEFEA5F95234EDB381342F9FECFF /* Products */, + D1CD7243A13CC5F0EBCF7434E72DC405 /* Targets Support Files */, + ); + sourceTree = "<group>"; + }; + 81EEC749343A32D9AABD3D43A5E3432E /* Support Files */ = { + isa = PBXGroup; + children = ( + A8589E48767597581056294CB938B043 /* Info.plist */, + 3DE2435666584AC8FB421A9493EE947E /* MASPreferences.modulemap */, + 59D92C57F5A2A0D7F7684294F61F384E /* MASPreferences.xcconfig */, + 15E24F59F5BF1D93B88559CE6FDCD69D /* MASPreferences-dummy.m */, + EBD5047055F2913D7879954318068002 /* MASPreferences-prefix.pch */, + B9A97FF1AD255831C3D35EF762EC3963 /* MASPreferences-umbrella.h */, + ); + name = "Support Files"; + path = "../Target Support Files/MASPreferences"; + sourceTree = "<group>"; + }; + 84B75A3EE41198EE802841C92D8FFA35 /* GRDB.swift */ = { + isa = PBXGroup; + children = ( + 97B6DC785F4AF05D64662BF84DF8A4C4 /* CGFloat.swift */, + 2008EFF5E8CD78BF04F92700384661BB /* Column.swift */, + 2FE55E91405466A2B73473046565A296 /* Configuration.swift */, + 0609DED75DE6CFF2D82338555B4E5A3B /* Cursor.swift */, + AA830A16FECE6E7A873C6709E18C12C8 /* Data.swift */, + 9753E560EAA431257EA6A208A6189F1C /* Database.swift */, + 909D02D461734E0F0AB95898A4024D87 /* DatabaseDateComponents.swift */, + 3D69051CE87133DF7943048BB905865C /* DatabaseError.swift */, + BFCCD4563D2890C2F24004E30E87F652 /* DatabaseFunction.swift */, + D83DE56D56DF7F33A58CDE3FBE5C74BC /* DatabaseMigrator.swift */, + 8D0D878085ECFDC95DB94F95C38CEF33 /* DatabasePool.swift */, + 89660E86ED40BAC0E1B18F677B38D934 /* DatabaseQueue.swift */, + 808D59870017B81D7C4BB491E46E2FB5 /* DatabaseReader.swift */, + F10F7E72A3C0476B5CBE7BCD76AEB544 /* DatabaseSchemaCache.swift */, + 990043F5B82A0419C8681BFC6DCF3BA5 /* DatabaseValue.swift */, + EFFBF27EB0F493F55534CDAA47439E6F /* DatabaseValueConvertible.swift */, + 5303BAC79922938BD120C6DC267F32C3 /* DatabaseValueConvertible+Decodable.swift */, + 8C2D1354EAEFF0B665ACC2ADFE3B9025 /* DatabaseValueConvertible+Encodable.swift */, + 345F727AEA9953BC160BCE131BEE6427 /* DatabaseValueConvertible+RawRepresentable.swift */, + 3F91BDEB2D405F45EC2D361057B16F7D /* DatabaseValueConvertible+ReferenceConvertible.swift */, + 62BFB7CBCB7EB2A55B3943699D8E30D8 /* DatabaseWriter.swift */, + CB0E864CE99D47F0FFC7C0A97D12D26C /* Date.swift */, + 05C32152421CEA4C13D11CA19C3627A7 /* FetchedRecordsController.swift */, + 8611935157B6EBA420E2C339F1C99B7D /* Fixits-0-84-0.swift */, + B9981D05528B0F8A77DBDB8D1C39AC2C /* Fixits-0-90-1.swift */, + 3AAF175727EB0D921FEE0B80A6BC1870 /* Fixits-0.101.1.swift */, + 5248BD7939C5BDDD4886DF1588A999BF /* Fixits-0.102.0.swift */, + 6ED9EA722D4F1776976D4BFBEB0BBC60 /* Fixits-0.109.0.swift */, + 911F15D7F42F8B451A0AB8DCCBDED5FB /* Fixits-1.2.swift */, + 507BBD5E05AFFB3CCEC645B291402119 /* Fixits-Swift2.swift */, + 2FE2A6C2A6D0C8752CF6155EF89A9F32 /* FTS3.swift */, + F16B07D6738B2921FC72E05C59FBA928 /* FTS3+QueryInterfaceRequest.swift */, + 14A4FE5E0F0883B56473CE2D57627E58 /* FTS3Pattern.swift */, + CCAD435CDE8E1DA04D186B5C30D21ADF /* FTS3TokenizerDescriptor.swift */, + 9D739B440A5ABF34D83780606966390A /* FTS4.swift */, + 4B9E910D1B4AE2A570378717CD26F6B0 /* FTS5.swift */, + D970DD209D94BFB0FF1104FE93E63CE3 /* FTS5+QueryInterface.swift */, + 1F9437AC3C5900FB1C5F0F23FA8DF24E /* FTS5CustomTokenizer.swift */, + C5B6579C6525893DF3C918F55CA5100B /* FTS5Pattern.swift */, + FF403126D6AD8F4F71602596FB86C4B7 /* FTS5Tokenizer.swift */, + 66B654975C4A527AB75CCABCDE0A00DA /* FTS5TokenizerDescriptor.swift */, + EF5181AF1444B08132A17637E28D5255 /* FTS5WrapperTokenizer.swift */, + C0FD700528DE5AC13168918C20F51190 /* GRDB.h */, + 0221306196EB6FE75412784C2196C5CA /* GRDB-Bridging.h */, + 3F539FFB32290F4DFF61D1DD8198EFAE /* grdb_config.h */, + 4085AE79FDBE5C4FDD656E2C3580CFF3 /* Migration.swift */, + 370626C546416F774136476700DE1038 /* NSData.swift */, + 0A736DEC1453B4125EFE28FCACE033A4 /* NSNull.swift */, + 145F60F2F4B4ADC0150F858314325F83 /* NSNumber.swift */, + D5E3FA503DE3C156EE18759FCBBCB92B /* NSString.swift */, + 91C0BBC5E24A2CD358DB23464123FD09 /* Persistable.swift */, + 756CD83642C176613222ED4F6407FA4E /* Persistable+Encodable.swift */, + FB282CFF6640F828AC6C371DD1F161D4 /* Pool.swift */, + EBC9A165C781CD0AAA4E579881930270 /* QueryInterfaceRequest.swift */, + 97BEAA6413FFCB46AC6B51770FE8C168 /* QueryInterfaceSelectQueryDefinition.swift */, + 9DCAC7083C7CF2E9D85BB9C7600139F2 /* ReadWriteBox.swift */, + 4728069A0AA3F69C0DAA76DC03FE5AB8 /* Record.swift */, + 48933C4790981A5B9C41C01C95C37036 /* Request.swift */, + 7B5E2EA9C94BAAD2DF20F479AD4B21DE /* Result.swift */, + C8A49C0DDB44E19ED62DD5DAED3C37B1 /* Row.swift */, + D3D9FFD14F2949364162F5568B74093E /* RowAdapter.swift */, + 23A783B2C6FB5AFD062C9546C0AFEE50 /* RowConvertible.swift */, + D0147059819728AFACFC2E4BE809B235 /* RowConvertible+Decodable.swift */, + BACAABB13C2049B29778580D51A8B921 /* RowConvertible+TableMapping.swift */, + DD6081E2379FA9B56747171B94180401 /* SchedulingWatchdog.swift */, + 2EA052A5ED7F6E02FB674087C3029623 /* SerializedDatabase.swift */, + CB717BC79D7B38246221EFE3E43E0FAB /* SQLCollatedExpression.swift */, + A62F2BADE8667533499FF2633F916702 /* SQLCollection.swift */, + D3ABD05CDD021FE5537B23CEFF951FDC /* SQLExpressible.swift */, + 591897BEA10D71FACD9D3ED7EFF88424 /* SQLExpression.swift */, + 5349F246A8B014B5AD1551E3BD7E4A2D /* SQLExpression+QueryInterface.swift */, + 0F5F59BC5036BDA8AFC885A3153B8CCD /* SQLFunctions.swift */, + AEC4BD3E87F3F9A8ED67476718C8BE6C /* SQLOperators.swift */, + 6F54ABE4B6F59407C9EAC318D1E30E33 /* SQLOrdering.swift */, + 5D34890E940700B6CBB4372925A5629E /* SQLSelectable.swift */, + B4B353141BFC1CCF8446F81E09DE99C8 /* SQLSelectable+QueryInterface.swift */, + 942965EB959AD61EFD82EA92F993292C /* SQLSpecificExpressible+QueryInterface.swift */, + 655D5D910E7C1570B9ACB2326AAFD7B0 /* StandardLibrary.swift */, + B1B3E456071D498DCDDFB074B684C03E /* Statement.swift */, + ACFB5649B721298AFD991FE3B9B45887 /* StatementColumnConvertible.swift */, + F6EFBAB00C4728232E692083A01126BE /* TableDefinition.swift */, + 62E970FCB53AD5114A3ED81BB895758B /* TableMapping.swift */, + 093AB0FF5E4B611961E2CC5C302DF649 /* URL.swift */, + 086D654A17B1E380BDB36A6630AFE941 /* Utils.swift */, + C9878D2CA813DFF2A735ED27855C6F95 /* UUID.swift */, + A744578CEA7FCC611331AC7A16ACA2AB /* VirtualTableModule.swift */, + E65D1B863C6B466A31526F9F1671C229 /* Support Files */, + ); + name = GRDB.swift; + path = GRDB.swift; + sourceTree = "<group>"; + }; + 92FC4590B3B6C6A2141FDC7D1DA8879E /* OS X */ = { + isa = PBXGroup; + children = ( + 6526E38143AA3C7320583E142AA64538 /* Cocoa.framework */, + 1E110E3518E2D475577D0E2056C8B05D /* Foundation.framework */, + ); + name = "OS X"; + sourceTree = "<group>"; + }; + A7E346F56A11FA831AFB44D09613A4D8 /* Pods-Doughnut */ = { + isa = PBXGroup; + children = ( + 54EEC492649EF779C5583CF7582ADC60 /* Info.plist */, + C2FCAEA387C94AF0A27D0AB48220ED2C /* Pods-Doughnut.modulemap */, + B4788A1A34E8963E736947E400DE9CC8 /* Pods-Doughnut-acknowledgements.markdown */, + BA3F9E0DFFA40C92040777A1E1F11F4C /* Pods-Doughnut-acknowledgements.plist */, + BEB0649B2A1A987642397C1C4E8D6133 /* Pods-Doughnut-dummy.m */, + CC2C9ADD34D3F9F4235FA3C4EEA8919B /* Pods-Doughnut-frameworks.sh */, + 3E015828D1204320A6E2A46DDD43A1A9 /* Pods-Doughnut-resources.sh */, + 5F570D72DB5F4A8CB79E6FDB1A647E42 /* Pods-Doughnut-umbrella.h */, + 38AAA3C7B1D07842DDC4CBE1D40E461B /* Pods-Doughnut.debug.xcconfig */, + B578A434E856D11E6137DB9016572807 /* Pods-Doughnut.release.xcconfig */, + 14E554C4DFEBD9EFE559F22253712B21 /* Pods-Doughnut.test.xcconfig */, + ); + name = "Pods-Doughnut"; + path = "Target Support Files/Pods-Doughnut"; + sourceTree = "<group>"; + }; + AE260A7F851B32589482D6FC96873BC8 /* Pods */ = { + isa = PBXGroup; + children = ( + 62640CA783BEA40D4B2E538D5A57F911 /* FeedKit */, + 84B75A3EE41198EE802841C92D8FFA35 /* GRDB.swift */, + 5FA58EECA2986FD8C41A97A90CEF787D /* MASPreferences */, + ); + name = Pods; + sourceTree = "<group>"; + }; + C1B00AD157485328ADFA6CA523A4A36C /* Support Files */ = { + isa = PBXGroup; + children = ( + 7FC743D751A0C8B97CBACB94F257B4D4 /* FeedKit.modulemap */, + 5DD14C34E748F3FFB1047DD6117B2B53 /* FeedKit.xcconfig */, + 6EF86E191DEBE5C06D6D5B8DAA56325F /* FeedKit-dummy.m */, + 4E89F40015E13C8A15B2B97ABC2E0D2A /* FeedKit-prefix.pch */, + EFFD3F77AA195135E9815DB402EE79BC /* FeedKit-umbrella.h */, + 62343700C604698F566303041B076C88 /* Info.plist */, + ); + name = "Support Files"; + path = "../Target Support Files/FeedKit"; + sourceTree = "<group>"; + }; + D1CD7243A13CC5F0EBCF7434E72DC405 /* Targets Support Files */ = { + isa = PBXGroup; + children = ( + A7E346F56A11FA831AFB44D09613A4D8 /* Pods-Doughnut */, + F1FBC0F71530BFA35F1271889ED373A1 /* Pods-DoughnutTests */, + 634D72C5571DCE0BE31053FFE4A19CAF /* Pods-DoughnutUITests */, + ); + name = "Targets Support Files"; + sourceTree = "<group>"; + }; + D648CE86F139C7CCFD55D5B8A03BE74B /* Frameworks */ = { + isa = PBXGroup; + children = ( + 92FC4590B3B6C6A2141FDC7D1DA8879E /* OS X */, + ); + name = Frameworks; + sourceTree = "<group>"; + }; + E65D1B863C6B466A31526F9F1671C229 /* Support Files */ = { + isa = PBXGroup; + children = ( + 4EBAD10A0F30CBA56EB0A8CDD93E800E /* GRDB.swift.modulemap */, + FB7947ADE87755EFDB2B745A668402C7 /* GRDB.swift.xcconfig */, + 16276F1176B56EFC5E0F542CED393716 /* GRDB.swift-dummy.m */, + C477BBD3E1538F96AEE26B08AAEBA830 /* GRDB.swift-prefix.pch */, + A23056F7698CAD7C802996DF49FC9F6E /* Info.plist */, + ); + name = "Support Files"; + path = "../Target Support Files/GRDB.swift"; + sourceTree = "<group>"; + }; + F1FBC0F71530BFA35F1271889ED373A1 /* Pods-DoughnutTests */ = { + isa = PBXGroup; + children = ( + 6440B327F4D6A16BE250F1BACF5B2713 /* Info.plist */, + 477AAF4A448C0D60EBCFC3CCF0401DE5 /* Pods-DoughnutTests.modulemap */, + 2D2FCCB544CB76805D48CCA928F7673D /* Pods-DoughnutTests-acknowledgements.markdown */, + 6EA26604087D6585F87D76A763B12361 /* Pods-DoughnutTests-acknowledgements.plist */, + 02DB42D9A5AEF3D105C570519A087040 /* Pods-DoughnutTests-dummy.m */, + 8D4D46CD90FCC006A723CCDD0E73B115 /* Pods-DoughnutTests-frameworks.sh */, + C7FAE255C5D7FEC3964E2F4B82430383 /* Pods-DoughnutTests-resources.sh */, + 1910CDF40C4CC9713A3B43B7858153DE /* Pods-DoughnutTests-umbrella.h */, + E5A83F6F7498D2CDF5B190A4F96CCB46 /* Pods-DoughnutTests.debug.xcconfig */, + DEFFD25D2E91CE72BB3A50FC6DDC958D /* Pods-DoughnutTests.release.xcconfig */, + EF2BBC2A5306F8C330A6FC3FE9F335D7 /* Pods-DoughnutTests.test.xcconfig */, + ); + name = "Pods-DoughnutTests"; + path = "Target Support Files/Pods-DoughnutTests"; + sourceTree = "<group>"; + }; + FF055A3110D449AA3D57409B2D6DE2D0 /* Resources */ = { + isa = PBXGroup; + children = ( + 0A6DC69A17F3FE58408AE4DCED195A96 /* MASPreferencesWindow.xib */, + ); + name = Resources; + sourceTree = "<group>"; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 230C6FF1F7148E2FA473F77FB94C8A0A /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A9EA14058E1624F4F9ECB170A15D2EA4 /* GRDB-Bridging.h in Headers */, + 6220B4E4C3A170FDE480BC6392A3A096 /* GRDB.h in Headers */, + 723DCDFAD209722869C4D46DE9F45527 /* grdb_config.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2405D5EB6587DF63C4CC37DA4E4CBBFE /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 9FA8120D435E6B6C75A94B32A2DD7DAD /* FeedKit-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 28C79F9C2173184F69D694913D2AD5A7 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 1242E894CDE5ABF50DA3CF1E629D4D0D /* Pods-DoughnutTests-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 67CFFA5F305D5062CB6AC2411379FA7F /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 4260E8B2EA1EB476EF1329D326ABED3C /* Pods-DoughnutUITests-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 912270990631A81BB7E8366BDAAE94FD /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 144BD0BDA4E1B52F2884500FE3D0DE84 /* MASPreferences-umbrella.h in Headers */, + D2E4F288BB8578A422530E91A32EF566 /* MASPreferences.h in Headers */, + 61B397687EE640E035029E53126EE94C /* MASPreferencesViewController.h in Headers */, + 73D7DE4E3DA284B129ECC2881C1AA74C /* MASPreferencesWindowController.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F65576A9F35E30CEB9FBE1C86947BB40 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + B0F6C08D31FCA77339FBE95174C281D2 /* Pods-Doughnut-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 24407B2D21B5CBB69BA7413CA12B6DFD /* MASPreferences */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7E073914876F28CC347DCFB4CD0CB57 /* Build configuration list for PBXNativeTarget "MASPreferences" */; + buildPhases = ( + 70230AEFF2F71EC2C7FC0A2CE8C81F6E /* Sources */, + 3BB15557A7688764EAEF2AB2771071BB /* Frameworks */, + 912270990631A81BB7E8366BDAAE94FD /* Headers */, + 0A1CCD62B771D8DAFA1C5ED30F20A7FD /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MASPreferences; + productName = MASPreferences; + productReference = 8867DA4F4C846385BD5959EED30A9AE6 /* MASPreferences.framework */; + productType = "com.apple.product-type.framework"; + }; + 27028E91F40BA717A57CDBFF74AD5FB8 /* GRDB.swift */ = { + isa = PBXNativeTarget; + buildConfigurationList = 515115E7AE4E229DE3CCE49DC9118664 /* Build configuration list for PBXNativeTarget "GRDB.swift" */; + buildPhases = ( + C7A8864A78749FB42B9BBFFA655A689C /* Sources */, + A7BA9AD4C3E97C2C60E90AD605DDE795 /* Frameworks */, + 230C6FF1F7148E2FA473F77FB94C8A0A /* Headers */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = GRDB.swift; + productName = GRDB.swift; + productReference = 9C10A891374D7ACA5D0AEF3D007FFDA3 /* GRDB.framework */; + productType = "com.apple.product-type.framework"; + }; + 7A21FF5D0C3EF67BFA81DDCC3CCCDFE3 /* Pods-DoughnutTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = B0D8F1B3A803CC247E11B3ADC3A59761 /* Build configuration list for PBXNativeTarget "Pods-DoughnutTests" */; + buildPhases = ( + BCBC55EFB39513AA6CE58C3101915079 /* Sources */, + A360AFF726A03C175427B0B89961E57A /* Frameworks */, + 28C79F9C2173184F69D694913D2AD5A7 /* Headers */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Pods-DoughnutTests"; + productName = "Pods-DoughnutTests"; + productReference = 9D933BADB401D42C579D4BB3E65E9F6B /* Pods_DoughnutTests.framework */; + productType = "com.apple.product-type.framework"; + }; + C3031E5FA8441015AE20C188F72D895F /* Pods-Doughnut */ = { + isa = PBXNativeTarget; + buildConfigurationList = F3CCE0E7DDFA3343B26E37D37F890089 /* Build configuration list for PBXNativeTarget "Pods-Doughnut" */; + buildPhases = ( + 6FCA0B6170D3C7403302A443416D9A59 /* Sources */, + F6DBAE7AF45696FFFFF66D1E304754A7 /* Frameworks */, + F65576A9F35E30CEB9FBE1C86947BB40 /* Headers */, + ); + buildRules = ( + ); + dependencies = ( + F738D43201A5EB06CFEB228B89167D34 /* PBXTargetDependency */, + A8601110B1FD104928D06E8052836295 /* PBXTargetDependency */, + F6C91C13031D998DE9887891E4126289 /* PBXTargetDependency */, + ); + name = "Pods-Doughnut"; + productName = "Pods-Doughnut"; + productReference = EA366C8B2AE0983FCFF8D03E243FA291 /* Pods_Doughnut.framework */; + productType = "com.apple.product-type.framework"; + }; + D9909DAA479A84B0AA45A9DF104A5B5B /* FeedKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = 276252C88C9926F579DE43538C97090B /* Build configuration list for PBXNativeTarget "FeedKit" */; + buildPhases = ( + DBEF97F38294B538CA60EEA9BA39D1AE /* Sources */, + 4C8C3CC1191C5C1B62A29278AA69BED0 /* Frameworks */, + 2405D5EB6587DF63C4CC37DA4E4CBBFE /* Headers */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FeedKit; + productName = FeedKit; + productReference = 9BC0D57CC36E24162EF8B0B7BF77D7CD /* FeedKit.framework */; + productType = "com.apple.product-type.framework"; + }; + DDC1A3D53143008CDB30E4F8FD8986B9 /* Pods-DoughnutUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7F191F066452F0123E3A856B6816F8D1 /* Build configuration list for PBXNativeTarget "Pods-DoughnutUITests" */; + buildPhases = ( + 6390B95B1DFA460D56AD5D1D2374201A /* Sources */, + 58D77401A8A48676FCA2C5BB50AA9342 /* Frameworks */, + 67CFFA5F305D5062CB6AC2411379FA7F /* Headers */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Pods-DoughnutUITests"; + productName = "Pods-DoughnutUITests"; + productReference = 9AE812A0D85492CDB53E68C143162F0B /* Pods_DoughnutUITests.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D41D8CD98F00B204E9800998ECF8427E /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0830; + LastUpgradeCheck = 0700; + }; + buildConfigurationList = 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 7DB346D0F39D3F0E887471402A8071AB; + productRefGroup = 3AE4BEFEA5F95234EDB381342F9FECFF /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D9909DAA479A84B0AA45A9DF104A5B5B /* FeedKit */, + 27028E91F40BA717A57CDBFF74AD5FB8 /* GRDB.swift */, + 24407B2D21B5CBB69BA7413CA12B6DFD /* MASPreferences */, + C3031E5FA8441015AE20C188F72D895F /* Pods-Doughnut */, + 7A21FF5D0C3EF67BFA81DDCC3CCCDFE3 /* Pods-DoughnutTests */, + DDC1A3D53143008CDB30E4F8FD8986B9 /* Pods-DoughnutUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 0A1CCD62B771D8DAFA1C5ED30F20A7FD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8ABB550E3E61D643C7E9EE85A7D0DA32 /* MASPreferencesWindow.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 6390B95B1DFA460D56AD5D1D2374201A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EB8967A64C01119DFEAAE338CB985F7B /* Pods-DoughnutUITests-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6FCA0B6170D3C7403302A443416D9A59 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BDE762D040D3F2C35C371861C892FB19 /* Pods-Doughnut-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 70230AEFF2F71EC2C7FC0A2CE8C81F6E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9420B9A41A54B52A10E74B42CFA732EF /* MASPreferences-dummy.m in Sources */, + B4D22075D83F92FC37723C4B7F007FD3 /* MASPreferencesWindowController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BCBC55EFB39513AA6CE58C3101915079 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ACB40D7CEC3FDB849FD234ED4424AFCE /* Pods-DoughnutTests-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C7A8864A78749FB42B9BBFFA655A689C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B3E72A5CA670E311394B99C9DF2B6079 /* CGFloat.swift in Sources */, + A0003DE469543BD0F0E90311114A68D0 /* Column.swift in Sources */, + EA1514C8236240D51A4A3775A3BD8E45 /* Configuration.swift in Sources */, + 2CEDE282E4B964FFA5E41BABFBA368A0 /* Cursor.swift in Sources */, + 8FD9329BFD54F9F18990F41446F7ED65 /* Data.swift in Sources */, + B8C5EB4FACF9CCB23B8D89F6DEFA7B1F /* Database.swift in Sources */, + 015A0EA63F06AE59C3EBA4C962B88228 /* DatabaseDateComponents.swift in Sources */, + 5E2325376771642437CE5C67781DE7BF /* DatabaseError.swift in Sources */, + 3E70411BE2F06354BB65A6712D17BE18 /* DatabaseFunction.swift in Sources */, + 1238583ABBFD5367A8E09B3020116996 /* DatabaseMigrator.swift in Sources */, + FE262D94E2F1F2752123D4785345D92F /* DatabasePool.swift in Sources */, + 1E86F1FCF01FE15CBD7E6B68EF452745 /* DatabaseQueue.swift in Sources */, + 7D3461BE566C3D6B6EBC80833CDCC270 /* DatabaseReader.swift in Sources */, + CFEEB09ADFF8D1D31138D90CF63DEB8B /* DatabaseSchemaCache.swift in Sources */, + 8B5DFC093DE87C292BB89F1A07255592 /* DatabaseValue.swift in Sources */, + E735F43DA1FF0E5F476787D414F1DDB9 /* DatabaseValueConvertible+Decodable.swift in Sources */, + B8C67017FBAFAE4E9BB374ED6B9B937A /* DatabaseValueConvertible+Encodable.swift in Sources */, + 16A2421302AD3C562A22ACF05DD35D27 /* DatabaseValueConvertible+RawRepresentable.swift in Sources */, + B0BA19538AA0574BEB853C901707C2D8 /* DatabaseValueConvertible+ReferenceConvertible.swift in Sources */, + CB5BEE1764A8BBF98ABC9BFD1970E31D /* DatabaseValueConvertible.swift in Sources */, + 472C7BE6804B003089D20FA686CCD1B5 /* DatabaseWriter.swift in Sources */, + 924C41137EC5BE2FF670AF5DB2450491 /* Date.swift in Sources */, + 850B1AA8CAE2960B8932379904BA70ED /* FetchedRecordsController.swift in Sources */, + 70B4649FF85E84EF23F63F1A96DBD103 /* Fixits-0-84-0.swift in Sources */, + 11CDD732FBEFFB6C222341F2E5D1FD43 /* Fixits-0-90-1.swift in Sources */, + 8A8AC3C2B2EE1545F308684E8B159667 /* Fixits-0.101.1.swift in Sources */, + 01560F02DF617E37AAEC99826AD225FD /* Fixits-0.102.0.swift in Sources */, + A1279CA0A2D65F19B122B64C523527CD /* Fixits-0.109.0.swift in Sources */, + AE3914F24305DA71BEBF5FA40FCA4A3A /* Fixits-1.2.swift in Sources */, + A8079AD54B0441CC55B1AA06140B17D0 /* Fixits-Swift2.swift in Sources */, + 9366DF79DD0869EDAC920306F1B03ECE /* FTS3+QueryInterfaceRequest.swift in Sources */, + B4BF75D727B278149028A2EA78F544A1 /* FTS3.swift in Sources */, + 45F03DD3DF08776DFC2B50BC7032BA52 /* FTS3Pattern.swift in Sources */, + 4DF4FBD1BA874A79A16B4B3EDECCC2BC /* FTS3TokenizerDescriptor.swift in Sources */, + 54681616749A98E8392A5C914A465B30 /* FTS4.swift in Sources */, + D63874368F0B2BEE40B9656EFC9149F2 /* FTS5+QueryInterface.swift in Sources */, + DCCC8F501653AF1E43791DD8D351B4B5 /* FTS5.swift in Sources */, + 86EFFB72730BF178331CFB823A40C2FA /* FTS5CustomTokenizer.swift in Sources */, + 8EC3B36BFD00D14373F3AF46BBE04275 /* FTS5Pattern.swift in Sources */, + 8C0E07823C530623AEEA281DA56A7944 /* FTS5Tokenizer.swift in Sources */, + E91553565A99BD2277DFB0233EDF196E /* FTS5TokenizerDescriptor.swift in Sources */, + B33BBB7886EB9FBF911A7883353F95AD /* FTS5WrapperTokenizer.swift in Sources */, + CC84C111E8758AC11758D20FDE5A3E50 /* GRDB.swift-dummy.m in Sources */, + 4EB569E625B83FD452793CD5B3EA2214 /* Migration.swift in Sources */, + E126FE58CF572A77B7F6EF9E076EA7B7 /* NSData.swift in Sources */, + EA71047F476046F5BBD51CEB6E14C928 /* NSNull.swift in Sources */, + 7A0F55BD37EF8A2301DDA48D3669C734 /* NSNumber.swift in Sources */, + 085B45834F82BF5A38B82B46969EF0C2 /* NSString.swift in Sources */, + 89853C218B46B1E6CC59A9775623AD0C /* Persistable+Encodable.swift in Sources */, + 21EAE6C8681B3A0F81898E9FB403A6AD /* Persistable.swift in Sources */, + 6C7660830812AB978AAA6891A856829F /* Pool.swift in Sources */, + 04670CE91916C62AB41DC2C3753554A4 /* QueryInterfaceRequest.swift in Sources */, + 5F6657A6965F6AA0CFEF516650EF8AA9 /* QueryInterfaceSelectQueryDefinition.swift in Sources */, + 701BD3E86F3478C8F4B19565FDB78206 /* ReadWriteBox.swift in Sources */, + 428B6EC43A65FA2BCB01E38583D6F9E0 /* Record.swift in Sources */, + 0B29835F8D0714DCAA2466F177F80815 /* Request.swift in Sources */, + 6B1758A594C156CBF6FE5243BDAB629F /* Result.swift in Sources */, + 97322C178DADD18D0871E4697684E7C6 /* Row.swift in Sources */, + 44FE6263FAE7839D8844B8891CF5B012 /* RowAdapter.swift in Sources */, + 40010EF45358B85D9197E80F8BD64E0C /* RowConvertible+Decodable.swift in Sources */, + 1363A9F5B7FB467080868AB86A16E081 /* RowConvertible+TableMapping.swift in Sources */, + F4A61C4AA6DD8E5B3C9D24CA1AD7D4B7 /* RowConvertible.swift in Sources */, + 87B89ABA2215F31972915C945A4B15BF /* SchedulingWatchdog.swift in Sources */, + 57C22769BBBCF1C5B827023A266EFC04 /* SerializedDatabase.swift in Sources */, + 7AAB56C1560351BE05625EFFDD864E67 /* SQLCollatedExpression.swift in Sources */, + 2356483F9343F03D920CD8EC98E3FE20 /* SQLCollection.swift in Sources */, + DFD4B4752557AF53513FAC771204B82E /* SQLExpressible.swift in Sources */, + 5B9FA1758B40DC061FE2CCBBBD1C0FF7 /* SQLExpression+QueryInterface.swift in Sources */, + F040E39758DD812809278E04584619A4 /* SQLExpression.swift in Sources */, + AEC27B0DE32675E1CE6CAA2BA2D269DE /* SQLFunctions.swift in Sources */, + 6ADE7E972B318F1BF25360B911A4A2E1 /* SQLOperators.swift in Sources */, + F8B67FE870F0A4C2800F9C708CB9C0FC /* SQLOrdering.swift in Sources */, + 424DE7EC40EE4D81431855BB3DB720CE /* SQLSelectable+QueryInterface.swift in Sources */, + 42D64C35C66C24D2B0E931ADB7ACF8B2 /* SQLSelectable.swift in Sources */, + A8844D4934B07CFFF5BF7E27A2CEB3EA /* SQLSpecificExpressible+QueryInterface.swift in Sources */, + 3BED62B38E4739DF3B70CB3B19A03DBC /* StandardLibrary.swift in Sources */, + 7EDFAFF87720EC79FE90C828C9E50808 /* Statement.swift in Sources */, + BF428B844532BB985EE026AB53DE45C2 /* StatementColumnConvertible.swift in Sources */, + 82DD7212EF146FB98A06666B4B1DE0B7 /* TableDefinition.swift in Sources */, + 21F05C2C1255B9B61A26263765F5236E /* TableMapping.swift in Sources */, + 75150B06211DDFE2B4E9545D3C385661 /* URL.swift in Sources */, + 01E54B49B17CBC817DD4B67BC5AA90E9 /* Utils.swift in Sources */, + 8F0CDA7E86B37D92A413FFFF6A97B526 /* UUID.swift in Sources */, + 88B83D679F350AD0B76B5EE328FE4888 /* VirtualTableModule.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DBEF97F38294B538CA60EEA9BA39D1AE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 088A60E704FDA697F822C63F40E3418E /* Array + Equatable.swift in Sources */, + 0B75D161AF26494D7FA03A54ED261F12 /* AtomFeed + mapAttributes.swift in Sources */, + 6807BEB050DDF6C95D15F5BE298D0912 /* AtomFeed + mapCharacters.swift in Sources */, + 0237D4A96BC7A1CA501D012F9BA46D79 /* AtomFeed.swift in Sources */, + F4D96A1919175849870478C68E87EC16 /* AtomFeedAuthor.swift in Sources */, + A55792C74416B10B3BED5E3F152C237D /* AtomFeedCategory.swift in Sources */, + 063DD0F9CB22847FF37EDD6CD47121C5 /* AtomFeedContributor.swift in Sources */, + 1F20A25728EEBDC0604F5FB1C0DC158A /* AtomFeedEntry.swift in Sources */, + 5C2F6E3CAFB6E9B5F6038C410E27DCB4 /* AtomFeedEntryAuthor.swift in Sources */, + 235EC9451E8E3A445F36EB5B205869A7 /* AtomFeedEntryCategory.swift in Sources */, + 9816499E2B73B99BD8E9BF60EA2EF9F5 /* AtomFeedEntryContent.swift in Sources */, + 0939AF851462FFFEAE4A6AE9B7CFF25B /* AtomFeedEntryContributor.swift in Sources */, + BE15CFBF5D40F77DD63F42CEBDE5768F /* AtomFeedEntryLink.swift in Sources */, + 6D6CA410EFDE19B29F6FCBB5B461A065 /* AtomFeedEntrySource.swift in Sources */, + 3BDFB8591F0C22E33614B68B83FBE2EF /* AtomFeedEntrySummary.swift in Sources */, + 7837556CF0F4AB4A4C1DE19C2E6EC6AC /* AtomFeedGenerator.swift in Sources */, + 2964C38653915FDF24BB610384B7701A /* AtomFeedLink.swift in Sources */, + 6BA8FB4777E72CCAC403764A560C8224 /* AtomFeedSubtitle.swift in Sources */, + 692C5264C6FFDA0BD31C442D85353A13 /* AtomPath.swift in Sources */, + 27F3AE895F9B086800E6E7FFF1781EDF /* ContentNamespace.swift in Sources */, + 3B9E7A56D2799317BD22BFF154457C60 /* Data + toUtf8.swift in Sources */, + D1B748E576E60967CCF12A0A3D94E684 /* DateSpec.swift in Sources */, + 3240C422D123E107E4EB7BD424BDB0CA /* DublinCoreNamespace.swift in Sources */, + 8C0F573FD1B7ECFA566ACE26D85214C8 /* FeedDataType.swift in Sources */, + C4C7790778E15ADC51D3DF9FCF2052D6 /* FeedKit-dummy.m in Sources */, + 863B5BC440F1B41F8E2C3A8DFDED4303 /* FeedParser.swift in Sources */, + 1D8F7BDC0B319B198D507A3896185FC1 /* FeedParserProtocol.swift in Sources */, + 9A17507AB3A58585BDF151E7E6309DED /* ISO8601DateFormatter.swift in Sources */, + 4C7D6B8EB9A400F6102DB2B26C18C0DC /* iTunesCategory.swift in Sources */, + 8CA1D287612D1D8DDA3A6B1B712F9474 /* iTunesImage.swift in Sources */, + 28A8BE7772EB0F6B66FCB719AEFF76F1 /* iTunesNamespace.swift in Sources */, + 58F773BE75A892FF637F10EC40B35FBB /* iTunesOwner.swift in Sources */, + E9066FA88EFD33C6D338B56F835C9928 /* iTunesSubCategory.swift in Sources */, + 800DECB7F8255CF8E62D7F9B035E7513 /* JSONFeed.swift in Sources */, + 3362791404D89590C03CED865758AC08 /* JSONFeedAttachment.swift in Sources */, + 2D19D16295FAD50FE799CFA228073D81 /* JSONFeedAuthor.swift in Sources */, + A81811A901C50179B355C866545761EB /* JSONFeedHub.swift in Sources */, + 24068B8E24BC88B37E17C39D64AD2D0E /* JSONFeedItem.swift in Sources */, + 8D37FC0CEF5D1351AA18A552AC5ED229 /* JSONFeedParser.swift in Sources */, + 2A0B4AF17D2210AFB43A490B8A800C6B /* MediaCategory.swift in Sources */, + FB73B7CE29C4591930FC99D1FAD85C39 /* MediaCommunity.swift in Sources */, + 6BFEF476AD33FE4A90F4292277CDB12F /* MediaContent.swift in Sources */, + 6F058921789AA8851CC4A16EB141F8F7 /* MediaCopyright.swift in Sources */, + D121C36948619925BF790E794251A96A /* MediaCredit.swift in Sources */, + 1EC1ED271E82FAA726773B1F31781EAC /* MediaDescription.swift in Sources */, + 205A1AF3BB4D7FF2DCF6BB25F360EB6D /* MediaEmbed.swift in Sources */, + 0D45DC3AB332EBB1E9E878092800EBE4 /* MediaGroup.swift in Sources */, + 6FA4DEDE899D108F6BCAA16CCFCFA1FC /* MediaHash.swift in Sources */, + A8B88DCBBAD0A2561D5259E449BBF39C /* MediaLicence.swift in Sources */, + 7F31A833F094F10E992B895E43B1BF5C /* MediaLocation.swift in Sources */, + AB3907FEDFD269386C02BB2B01E4693C /* MediaNamespace.swift in Sources */, + 2F6AA8C1835345FFF75D91065B996706 /* MediaParam.swift in Sources */, + D40BA69B99E2928474C4728EF09159AF /* MediaPeerLink.swift in Sources */, + D54011128F08B3915EAF46BCC929910B /* MediaPlayer.swift in Sources */, + 0EFBA044F184723BCF120E94B5DF5AA4 /* MediaPrice.swift in Sources */, + 90564EF1A72FE9DF7EBCC06AA9F16411 /* MediaRating.swift in Sources */, + C47C1EF09C23D5B2495D8BC790BEBB39 /* MediaRestriction.swift in Sources */, + 2F7B5A9085482D838DCF6ECE0A3D137B /* MediaRights.swift in Sources */, + 586C16521FF293184DE95268EC61B8FC /* MediaScene.swift in Sources */, + AD1B1B307DE44CAB2C11625292078CE8 /* MediaStarRating.swift in Sources */, + 2CB5E4AD8990D06ED01F24CA7D0B122D /* MediaStatistics.swift in Sources */, + 076A1ECDB671FAB22AAF6312479A349B /* MediaStatus.swift in Sources */, + B5D830E9CACC53DD91DE33F64E286996 /* MediaSubTitle.swift in Sources */, + 5C229A7FF23962E233C9D76A8AF625CA /* MediaTag.swift in Sources */, + D0A187A46E4282A6D6DD477A16493C9B /* MediaText.swift in Sources */, + 3EE1F8E4698066084D9CAC51FDEF53C6 /* MediaThumbnail.swift in Sources */, + 9C2E7D4708BEDF7064F62626C0F9547F /* MediaTitle.swift in Sources */, + EC906E518EFC6E595257D5E773A88E21 /* ParserError.swift in Sources */, + 8318410374932897227C4A711533CB38 /* RDFPath.swift in Sources */, + 71C6DACF27FC54A115C50456E0D07512 /* Result.swift in Sources */, + 644B7F42B2B174F0D4B2F23981E9A914 /* RFC3339DateFormatter.swift in Sources */, + B97A254403232FA34480F353A94203F0 /* RFC822DateFormatter.swift in Sources */, + 39118E3F66EE193B092373438388337D /* RSSFeed + mapAttributes.swift in Sources */, + C07ADD6F4B131C7545AE75B7E660102C /* RSSFeed + mapCharacters.swift in Sources */, + 1873E44626B3D7D35BB567BA2481DFEB /* RSSFeed.swift in Sources */, + 8D96AD1FE2E42E51F4C219E05B2BE1F4 /* RSSFeedCategory.swift in Sources */, + 063A8F9E6A28D0C87C2FEBFE1BB69232 /* RSSFeedCloud.swift in Sources */, + 61C19C012C3834BA7ED7DE2618CFADC2 /* RSSFeedImage.swift in Sources */, + AF4303F69D4D487662B7421C51327EBF /* RSSFeedItem.swift in Sources */, + 2417FF905183DC1A71828FB7E609568A /* RSSFeedItemCategory.swift in Sources */, + F8A43534BA2F83852C8D27FE18854020 /* RSSFeedItemEnclosure.swift in Sources */, + 5C204DCE823928C66C91455E6C07E02E /* RSSFeedItemGUID.swift in Sources */, + A89ACFFC97857A07F5A5A7A5469DE628 /* RSSFeedItemSource.swift in Sources */, + 3CB6C7436E0C5CCBBA6FE4A3C3A059F8 /* RSSFeedSkipDay.swift in Sources */, + 32C4181080FE75CECF7FD5A31906CD25 /* RSSFeedSkipHour.swift in Sources */, + C335862F48E6D02D099DFCFA3D94DCC4 /* RSSFeedTextInput.swift in Sources */, + 37A6415DA362F829BF608437FC03CB1F /* RSSPath.swift in Sources */, + 3C40EDBD4D8BF1AFB414770B4FC2AC85 /* String + toBool.swift in Sources */, + E4D76E6F4E0988D1B5979690E37CA564 /* String + toDate.swift in Sources */, + FFC2EBC4D36B69DF40D37AC0AB8DEE62 /* String + toDuration.swift in Sources */, + 99F493FA1DB73B54ACF8F8A57514571E /* SyndicationNamespace.swift in Sources */, + A4977E51D9C9B6FD78141228BF959FF7 /* SyndicationUpdatePeriod.swift in Sources */, + 4108F7B95ADE8A41A3832AFF6EC37BF9 /* XMLFeedParser.swift in Sources */, + 6D7D4A8899C29A405C216F77B65AC572 /* XMLFeedType.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + A8601110B1FD104928D06E8052836295 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = GRDB.swift; + target = 27028E91F40BA717A57CDBFF74AD5FB8 /* GRDB.swift */; + targetProxy = 290B43CF676979C282C868920F519984 /* PBXContainerItemProxy */; + }; + F6C91C13031D998DE9887891E4126289 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = MASPreferences; + target = 24407B2D21B5CBB69BA7413CA12B6DFD /* MASPreferences */; + targetProxy = 81E5B64DAEC450F071399B80220563DC /* PBXContainerItemProxy */; + }; + F738D43201A5EB06CFEB228B89167D34 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = FeedKit; + target = D9909DAA479A84B0AA45A9DF104A5B5B /* FeedKit */; + targetProxy = 3E25442A23E392C08E1C9B2CCD240D2B /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 0A6DC69A17F3FE58408AE4DCED195A96 /* MASPreferencesWindow.xib */ = { + isa = PBXVariantGroup; + children = ( + 1D8A551FBE9CBF40BB5D7CE0D7EDD04D /* MASPreferencesWindow.xib */, + ); + name = MASPreferencesWindow.xib; + path = Framework; + sourceTree = "<group>"; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 0198FB7B50961830F27722B511E9FDA1 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5DD14C34E748F3FFB1047DD6117B2B53 /* FeedKit.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_PREFIX_HEADER = "Target Support Files/FeedKit/FeedKit-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/FeedKit/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MODULEMAP_FILE = "Target Support Files/FeedKit/FeedKit.modulemap"; + PRODUCT_NAME = FeedKit; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 0E874FC1E11EA46A506D32C67EC68439 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGNING_REQUIRED = NO; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_RELEASE=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = NO_SIGNING/; + STRIP_INSTALLED_PRODUCT = NO; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Release; + }; + 1760DE4C8FAE313B8BBE8B509AE21AE2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGNING_REQUIRED = NO; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_DEBUG=1", + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = NO_SIGNING/; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Debug; + }; + 1F4E45749FAA8325623020B4A50BEE14 /* Test */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 59D92C57F5A2A0D7F7684294F61F384E /* MASPreferences.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_PREFIX_HEADER = "Target Support Files/MASPreferences/MASPreferences-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/MASPreferences/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.6; + MODULEMAP_FILE = "Target Support Files/MASPreferences/MASPreferences.modulemap"; + PRODUCT_NAME = MASPreferences; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 4.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Test; + }; + 23FBE84F95BB92BBEA9DCFD7CF9862CE /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FB7947ADE87755EFDB2B745A668402C7 /* GRDB.swift.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_PREFIX_HEADER = "Target Support Files/GRDB.swift/GRDB.swift-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/GRDB.swift/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + MODULEMAP_FILE = "Target Support Files/GRDB.swift/GRDB.swift.modulemap"; + PRODUCT_NAME = GRDB; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 2823C9F40E2D3B0E02AF2640BE17D727 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DEFFD25D2E91CE72BB3A50FC6DDC958D /* Pods-DoughnutTests.release.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = "Target Support Files/Pods-DoughnutTests/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MODULEMAP_FILE = "Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = Pods_DoughnutTests; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 381D05C0E631411E0291CF88561BB1BD /* Test */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0202316A0F9E2D5123231CC9B185FA48 /* Pods-DoughnutUITests.test.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = "Target Support Files/Pods-DoughnutUITests/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MODULEMAP_FILE = "Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = Pods_DoughnutUITests; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Test; + }; + 38E9782F9B8F91ECD9A9BF2954004CCD /* Test */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGNING_REQUIRED = NO; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_TEST=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = NO_SIGNING/; + STRIP_INSTALLED_PRODUCT = NO; + }; + name = Test; + }; + 3C322939832B096A8F5B3FF55009556A /* Test */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 14E554C4DFEBD9EFE559F22253712B21 /* Pods-Doughnut.test.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = "Target Support Files/Pods-Doughnut/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MODULEMAP_FILE = "Target Support Files/Pods-Doughnut/Pods-Doughnut.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = Pods_Doughnut; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Test; + }; + 450F4AA47BA223564318A1F20698C93B /* Test */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FB7947ADE87755EFDB2B745A668402C7 /* GRDB.swift.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_PREFIX_HEADER = "Target Support Files/GRDB.swift/GRDB.swift-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/GRDB.swift/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + MODULEMAP_FILE = "Target Support Files/GRDB.swift/GRDB.swift.modulemap"; + PRODUCT_NAME = GRDB; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Test; + }; + 5426935E81E0215E56BBD28AA9789E1D /* Test */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5DD14C34E748F3FFB1047DD6117B2B53 /* FeedKit.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_PREFIX_HEADER = "Target Support Files/FeedKit/FeedKit-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/FeedKit/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MODULEMAP_FILE = "Target Support Files/FeedKit/FeedKit.modulemap"; + PRODUCT_NAME = FeedKit; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Test; + }; + 571A4B9360B767B4D65244A16D43FBEB /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E5A83F6F7498D2CDF5B190A4F96CCB46 /* Pods-DoughnutTests.debug.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = "Target Support Files/Pods-DoughnutTests/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MODULEMAP_FILE = "Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = Pods_DoughnutTests; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 8931E7D5D965BD9C739E8F1857159B90 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 47945A421975422B7C38AFF86ABC741C /* Pods-DoughnutUITests.debug.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = "Target Support Files/Pods-DoughnutUITests/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MODULEMAP_FILE = "Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = Pods_DoughnutUITests; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 8F6389541589E9ED49CAC02CD651ED82 /* Test */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EF2BBC2A5306F8C330A6FC3FE9F335D7 /* Pods-DoughnutTests.test.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = "Target Support Files/Pods-DoughnutTests/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MODULEMAP_FILE = "Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = Pods_DoughnutTests; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Test; + }; + 941C33EFF07C9B881E38B96A2232ADBA /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5DD14C34E748F3FFB1047DD6117B2B53 /* FeedKit.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_PREFIX_HEADER = "Target Support Files/FeedKit/FeedKit-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/FeedKit/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MODULEMAP_FILE = "Target Support Files/FeedKit/FeedKit.modulemap"; + PRODUCT_NAME = FeedKit; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + A44E2A7AD5EA9ED56EF8B900800AAFA1 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 59D92C57F5A2A0D7F7684294F61F384E /* MASPreferences.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_PREFIX_HEADER = "Target Support Files/MASPreferences/MASPreferences-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/MASPreferences/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.6; + MODULEMAP_FILE = "Target Support Files/MASPreferences/MASPreferences.modulemap"; + PRODUCT_NAME = MASPreferences; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 4.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + BF58164C1CC5483D46BF7B3763B5DADF /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 59D92C57F5A2A0D7F7684294F61F384E /* MASPreferences.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_PREFIX_HEADER = "Target Support Files/MASPreferences/MASPreferences-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/MASPreferences/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.6; + MODULEMAP_FILE = "Target Support Files/MASPreferences/MASPreferences.modulemap"; + PRODUCT_NAME = MASPreferences; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 4.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + EDF75CC03EC6C388A965C914430BC04C /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B578A434E856D11E6137DB9016572807 /* Pods-Doughnut.release.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = "Target Support Files/Pods-Doughnut/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MODULEMAP_FILE = "Target Support Files/Pods-Doughnut/Pods-Doughnut.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = Pods_Doughnut; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + EFD115C7EE6D8DA3E5C11C4723FE7C30 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FB7947ADE87755EFDB2B745A668402C7 /* GRDB.swift.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_PREFIX_HEADER = "Target Support Files/GRDB.swift/GRDB.swift-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/GRDB.swift/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + MODULEMAP_FILE = "Target Support Files/GRDB.swift/GRDB.swift.modulemap"; + PRODUCT_NAME = GRDB; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + F46F298388ACBCF0A36171E8B53EFC95 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 38AAA3C7B1D07842DDC4CBE1D40E461B /* Pods-Doughnut.debug.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = "Target Support Files/Pods-Doughnut/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MODULEMAP_FILE = "Target Support Files/Pods-Doughnut/Pods-Doughnut.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = Pods_Doughnut; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + FB1552C617BDB403FD03CD33774ABECB /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 52D381BB21B489004AC222F78F581533 /* Pods-DoughnutUITests.release.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = "Target Support Files/Pods-DoughnutUITests/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MODULEMAP_FILE = "Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = Pods_DoughnutUITests; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 276252C88C9926F579DE43538C97090B /* Build configuration list for PBXNativeTarget "FeedKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 941C33EFF07C9B881E38B96A2232ADBA /* Debug */, + 0198FB7B50961830F27722B511E9FDA1 /* Release */, + 5426935E81E0215E56BBD28AA9789E1D /* Test */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1760DE4C8FAE313B8BBE8B509AE21AE2 /* Debug */, + 0E874FC1E11EA46A506D32C67EC68439 /* Release */, + 38E9782F9B8F91ECD9A9BF2954004CCD /* Test */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 515115E7AE4E229DE3CCE49DC9118664 /* Build configuration list for PBXNativeTarget "GRDB.swift" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 23FBE84F95BB92BBEA9DCFD7CF9862CE /* Debug */, + EFD115C7EE6D8DA3E5C11C4723FE7C30 /* Release */, + 450F4AA47BA223564318A1F20698C93B /* Test */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7F191F066452F0123E3A856B6816F8D1 /* Build configuration list for PBXNativeTarget "Pods-DoughnutUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8931E7D5D965BD9C739E8F1857159B90 /* Debug */, + FB1552C617BDB403FD03CD33774ABECB /* Release */, + 381D05C0E631411E0291CF88561BB1BD /* Test */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B0D8F1B3A803CC247E11B3ADC3A59761 /* Build configuration list for PBXNativeTarget "Pods-DoughnutTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 571A4B9360B767B4D65244A16D43FBEB /* Debug */, + 2823C9F40E2D3B0E02AF2640BE17D727 /* Release */, + 8F6389541589E9ED49CAC02CD651ED82 /* Test */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F3CCE0E7DDFA3343B26E37D37F890089 /* Build configuration list for PBXNativeTarget "Pods-Doughnut" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F46F298388ACBCF0A36171E8B53EFC95 /* Debug */, + EDF75CC03EC6C388A965C914430BC04C /* Release */, + 3C322939832B096A8F5B3FF55009556A /* Test */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F7E073914876F28CC347DCFB4CD0CB57 /* Build configuration list for PBXNativeTarget "MASPreferences" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A44E2A7AD5EA9ED56EF8B900800AAFA1 /* Debug */, + BF58164C1CC5483D46BF7B3763B5DADF /* Release */, + 1F4E45749FAA8325623020B4A50BEE14 /* Test */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = D41D8CD98F00B204E9800998ECF8427E /* Project object */; +} diff --git a/Pods/Target Support Files/FeedKit/FeedKit-dummy.m b/Pods/Target Support Files/FeedKit/FeedKit-dummy.m new file mode 100644 index 0000000..7a788dd --- /dev/null +++ b/Pods/Target Support Files/FeedKit/FeedKit-dummy.m @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> +@interface PodsDummy_FeedKit : NSObject +@end +@implementation PodsDummy_FeedKit +@end diff --git a/Pods/Target Support Files/FeedKit/FeedKit-prefix.pch b/Pods/Target Support Files/FeedKit/FeedKit-prefix.pch new file mode 100644 index 0000000..082f8af --- /dev/null +++ b/Pods/Target Support Files/FeedKit/FeedKit-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import <Cocoa/Cocoa.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/Pods/Target Support Files/FeedKit/FeedKit-umbrella.h b/Pods/Target Support Files/FeedKit/FeedKit-umbrella.h new file mode 100644 index 0000000..7be7d51 --- /dev/null +++ b/Pods/Target Support Files/FeedKit/FeedKit-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import <Cocoa/Cocoa.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double FeedKitVersionNumber; +FOUNDATION_EXPORT const unsigned char FeedKitVersionString[]; + diff --git a/Pods/Target Support Files/FeedKit/FeedKit.modulemap b/Pods/Target Support Files/FeedKit/FeedKit.modulemap new file mode 100644 index 0000000..ecc6d00 --- /dev/null +++ b/Pods/Target Support Files/FeedKit/FeedKit.modulemap @@ -0,0 +1,6 @@ +framework module FeedKit { + umbrella header "FeedKit-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/FeedKit/FeedKit.xcconfig b/Pods/Target Support Files/FeedKit/FeedKit.xcconfig new file mode 100644 index 0000000..52c9c11 --- /dev/null +++ b/Pods/Target Support Files/FeedKit/FeedKit.xcconfig @@ -0,0 +1,11 @@ +CODE_SIGN_IDENTITY = +CONFIGURATION_BUILD_DIR = $PODS_CONFIGURATION_BUILD_DIR/FeedKit +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public" +OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" +PODS_BUILD_DIR = $BUILD_DIR +PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/FeedKit +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES diff --git a/Pods/Target Support Files/FeedKit/Info.plist b/Pods/Target Support Files/FeedKit/Info.plist new file mode 100644 index 0000000..3f2c147 --- /dev/null +++ b/Pods/Target Support Files/FeedKit/Info.plist @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleShortVersionString</key> + <string>7.0.1</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>${CURRENT_PROJECT_VERSION}</string> + <key>NSPrincipalClass</key> + <string></string> +</dict> +</plist> diff --git a/Pods/Target Support Files/GRDB.swift/GRDB.swift-dummy.m b/Pods/Target Support Files/GRDB.swift/GRDB.swift-dummy.m new file mode 100644 index 0000000..a7dc8db --- /dev/null +++ b/Pods/Target Support Files/GRDB.swift/GRDB.swift-dummy.m @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> +@interface PodsDummy_GRDB_swift : NSObject +@end +@implementation PodsDummy_GRDB_swift +@end diff --git a/Pods/Target Support Files/GRDB.swift/GRDB.swift-prefix.pch b/Pods/Target Support Files/GRDB.swift/GRDB.swift-prefix.pch new file mode 100644 index 0000000..082f8af --- /dev/null +++ b/Pods/Target Support Files/GRDB.swift/GRDB.swift-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import <Cocoa/Cocoa.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/Pods/Target Support Files/GRDB.swift/GRDB.swift.modulemap b/Pods/Target Support Files/GRDB.swift/GRDB.swift.modulemap new file mode 100644 index 0000000..04ac555 --- /dev/null +++ b/Pods/Target Support Files/GRDB.swift/GRDB.swift.modulemap @@ -0,0 +1,8 @@ +framework module GRDB { + umbrella header "GRDB.h" + + export * + module * { export * } + + header "grdb_config.h" +} diff --git a/Pods/Target Support Files/GRDB.swift/GRDB.swift.xcconfig b/Pods/Target Support Files/GRDB.swift/GRDB.swift.xcconfig new file mode 100644 index 0000000..3b9b27c --- /dev/null +++ b/Pods/Target Support Files/GRDB.swift/GRDB.swift.xcconfig @@ -0,0 +1,12 @@ +CODE_SIGN_IDENTITY = +CONFIGURATION_BUILD_DIR = $PODS_CONFIGURATION_BUILD_DIR/GRDB.swift +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public" +OTHER_LDFLAGS = -l"sqlite3" -framework "Foundation" +OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" +PODS_BUILD_DIR = $BUILD_DIR +PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/GRDB.swift +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES diff --git a/Pods/Target Support Files/GRDB.swift/Info.plist b/Pods/Target Support Files/GRDB.swift/Info.plist new file mode 100644 index 0000000..0a12077 --- /dev/null +++ b/Pods/Target Support Files/GRDB.swift/Info.plist @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleShortVersionString</key> + <string>2.0.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>${CURRENT_PROJECT_VERSION}</string> + <key>NSPrincipalClass</key> + <string></string> +</dict> +</plist> diff --git a/Pods/Target Support Files/MASPreferences/Info.plist b/Pods/Target Support Files/MASPreferences/Info.plist new file mode 100644 index 0000000..19f73ea --- /dev/null +++ b/Pods/Target Support Files/MASPreferences/Info.plist @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleShortVersionString</key> + <string>1.2.1</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>${CURRENT_PROJECT_VERSION}</string> + <key>NSPrincipalClass</key> + <string></string> +</dict> +</plist> diff --git a/Pods/Target Support Files/MASPreferences/MASPreferences-dummy.m b/Pods/Target Support Files/MASPreferences/MASPreferences-dummy.m new file mode 100644 index 0000000..0c31461 --- /dev/null +++ b/Pods/Target Support Files/MASPreferences/MASPreferences-dummy.m @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> +@interface PodsDummy_MASPreferences : NSObject +@end +@implementation PodsDummy_MASPreferences +@end diff --git a/Pods/Target Support Files/MASPreferences/MASPreferences-prefix.pch b/Pods/Target Support Files/MASPreferences/MASPreferences-prefix.pch new file mode 100644 index 0000000..082f8af --- /dev/null +++ b/Pods/Target Support Files/MASPreferences/MASPreferences-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import <Cocoa/Cocoa.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/Pods/Target Support Files/MASPreferences/MASPreferences-umbrella.h b/Pods/Target Support Files/MASPreferences/MASPreferences-umbrella.h new file mode 100644 index 0000000..4155283 --- /dev/null +++ b/Pods/Target Support Files/MASPreferences/MASPreferences-umbrella.h @@ -0,0 +1,19 @@ +#ifdef __OBJC__ +#import <Cocoa/Cocoa.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + +#import "MASPreferences.h" +#import "MASPreferencesViewController.h" +#import "MASPreferencesWindowController.h" + +FOUNDATION_EXPORT double MASPreferencesVersionNumber; +FOUNDATION_EXPORT const unsigned char MASPreferencesVersionString[]; + diff --git a/Pods/Target Support Files/MASPreferences/MASPreferences.modulemap b/Pods/Target Support Files/MASPreferences/MASPreferences.modulemap new file mode 100644 index 0000000..0042371 --- /dev/null +++ b/Pods/Target Support Files/MASPreferences/MASPreferences.modulemap @@ -0,0 +1,6 @@ +framework module MASPreferences { + umbrella header "MASPreferences-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/MASPreferences/MASPreferences.xcconfig b/Pods/Target Support Files/MASPreferences/MASPreferences.xcconfig new file mode 100644 index 0000000..6b9442e --- /dev/null +++ b/Pods/Target Support Files/MASPreferences/MASPreferences.xcconfig @@ -0,0 +1,10 @@ +CODE_SIGN_IDENTITY = +CONFIGURATION_BUILD_DIR = $PODS_CONFIGURATION_BUILD_DIR/MASPreferences +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public" +PODS_BUILD_DIR = $BUILD_DIR +PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/MASPreferences +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES diff --git a/Pods/Target Support Files/Pods-Doughnut/Info.plist b/Pods/Target Support Files/Pods-Doughnut/Info.plist new file mode 100644 index 0000000..2243fe6 --- /dev/null +++ b/Pods/Target Support Files/Pods-Doughnut/Info.plist @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleShortVersionString</key> + <string>1.0.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>${CURRENT_PROJECT_VERSION}</string> + <key>NSPrincipalClass</key> + <string></string> +</dict> +</plist> diff --git a/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-acknowledgements.markdown b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-acknowledgements.markdown new file mode 100644 index 0000000..3e46c1c --- /dev/null +++ b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-acknowledgements.markdown @@ -0,0 +1,67 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## FeedKit + +The MIT License (MIT) + +Copyright (c) 2017 Nuno Manuel Dias + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +## GRDB.swift + +Copyright (C) 2017 Gwendal Roué + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +## MASPreferences + +MASPreferences is licensed under the 2-clause BSD license. + +Copyright (c) 2016 Vadim Shpakovski. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Generated by CocoaPods - https://cocoapods.org diff --git a/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-acknowledgements.plist b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-acknowledgements.plist new file mode 100644 index 0000000..e7cb69b --- /dev/null +++ b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-acknowledgements.plist @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>PreferenceSpecifiers</key> + <array> + <dict> + <key>FooterText</key> + <string>This application makes use of the following third party libraries:</string> + <key>Title</key> + <string>Acknowledgements</string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + <dict> + <key>FooterText</key> + <string>The MIT License (MIT) + +Copyright (c) 2017 Nuno Manuel Dias + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</string> + <key>License</key> + <string>MIT</string> + <key>Title</key> + <string>FeedKit</string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + <dict> + <key>FooterText</key> + <string>Copyright (C) 2017 Gwendal Roué + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +</string> + <key>License</key> + <string>MIT</string> + <key>Title</key> + <string>GRDB.swift</string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + <dict> + <key>FooterText</key> + <string>MASPreferences is licensed under the 2-clause BSD license. + +Copyright (c) 2016 Vadim Shpakovski. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</string> + <key>License</key> + <string>BSD</string> + <key>Title</key> + <string>MASPreferences</string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + <dict> + <key>FooterText</key> + <string>Generated by CocoaPods - https://cocoapods.org</string> + <key>Title</key> + <string></string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + </array> + <key>StringsTable</key> + <string>Acknowledgements</string> + <key>Title</key> + <string>Acknowledgements</string> +</dict> +</plist> diff --git a/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-dummy.m b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-dummy.m new file mode 100644 index 0000000..14928c4 --- /dev/null +++ b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-dummy.m @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> +@interface PodsDummy_Pods_Doughnut : NSObject +@end +@implementation PodsDummy_Pods_Doughnut +@end diff --git a/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-frameworks.sh b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-frameworks.sh new file mode 100755 index 0000000..886ff8a --- /dev/null +++ b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-frameworks.sh @@ -0,0 +1,121 @@ +#!/bin/sh +set -e + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink "${source}")" + fi + + # Use filter instead of exclude so missing patterns don't throw errors. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} + +# Copies the dSYM of a vendored framework +install_dsym() { + local source="$1" + if [ -r "$source" ]; then + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DWARF_DSYM_FOLDER_PATH}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DWARF_DSYM_FOLDER_PATH}" + fi +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identitiy + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements '$1'" + + if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + code_sign_cmd="$code_sign_cmd &" + fi + echo "$code_sign_cmd" + eval "$code_sign_cmd" + fi +} + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + # Get architectures for current file + archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | rev)" + stripped="" + for arch in $archs; do + if ! [[ "${ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" || exit 1 + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi +} + + +if [[ "$CONFIGURATION" == "Debug" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/FeedKit/FeedKit.framework" + install_framework "${BUILT_PRODUCTS_DIR}/GRDB.swift/GRDB.framework" + install_framework "${BUILT_PRODUCTS_DIR}/MASPreferences/MASPreferences.framework" +fi +if [[ "$CONFIGURATION" == "Test" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/FeedKit/FeedKit.framework" + install_framework "${BUILT_PRODUCTS_DIR}/GRDB.swift/GRDB.framework" + install_framework "${BUILT_PRODUCTS_DIR}/MASPreferences/MASPreferences.framework" +fi +if [[ "$CONFIGURATION" == "Release" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/FeedKit/FeedKit.framework" + install_framework "${BUILT_PRODUCTS_DIR}/GRDB.swift/GRDB.framework" + install_framework "${BUILT_PRODUCTS_DIR}/MASPreferences/MASPreferences.framework" +fi +if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + wait +fi diff --git a/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-resources.sh b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-resources.sh new file mode 100755 index 0000000..a7df440 --- /dev/null +++ b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-resources.sh @@ -0,0 +1,106 @@ +#!/bin/sh +set -e + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + +RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt +> "$RESOURCES_TO_COPY" + +XCASSET_FILES=() + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +case "${TARGETED_DEVICE_FAMILY}" in + 1,2) + TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" + ;; + 1) + TARGET_DEVICE_ARGS="--target-device iphone" + ;; + 2) + TARGET_DEVICE_ARGS="--target-device ipad" + ;; + 3) + TARGET_DEVICE_ARGS="--target-device tv" + ;; + 4) + TARGET_DEVICE_ARGS="--target-device watch" + ;; + *) + TARGET_DEVICE_ARGS="--target-device mac" + ;; +esac + +install_resource() +{ + if [[ "$1" = /* ]] ; then + RESOURCE_PATH="$1" + else + RESOURCE_PATH="${PODS_ROOT}/$1" + fi + if [[ ! -e "$RESOURCE_PATH" ]] ; then + cat << EOM +error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. +EOM + exit 1 + fi + case $RESOURCE_PATH in + *.storyboard) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.xib) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.framework) + echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true + mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + ;; + *.xcdatamodel) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" || true + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" + ;; + *.xcdatamodeld) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" || true + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" + ;; + *.xcmappingmodel) + echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" || true + xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" + ;; + *.xcassets) + ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" + XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") + ;; + *) + echo "$RESOURCE_PATH" || true + echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" + ;; + esac +} + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then + mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi +rm -f "$RESOURCES_TO_COPY" + +if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ] +then + # Find all other xcassets (this unfortunately includes those of path pods and other targets). + OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) + while read line; do + if [[ $line != "${PODS_ROOT}*" ]]; then + XCASSET_FILES+=("$line") + fi + done <<<"$OTHER_XCASSETS" + + printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi diff --git a/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-umbrella.h b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-umbrella.h new file mode 100644 index 0000000..5568839 --- /dev/null +++ b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import <Cocoa/Cocoa.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_DoughnutVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_DoughnutVersionString[]; + diff --git a/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut.debug.xcconfig b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut.debug.xcconfig new file mode 100644 index 0000000..32f23a2 --- /dev/null +++ b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut.debug.xcconfig @@ -0,0 +1,12 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CODE_SIGN_IDENTITY = +FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/FeedKit" "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift" "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks' +OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/FeedKit/FeedKit.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift/GRDB.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences/MASPreferences.framework/Headers" +OTHER_LDFLAGS = $(inherited) -framework "FeedKit" -framework "GRDB" -framework "MASPreferences" +OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" +PODS_BUILD_DIR = $BUILD_DIR +PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut.modulemap b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut.modulemap new file mode 100644 index 0000000..54a8a58 --- /dev/null +++ b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut.modulemap @@ -0,0 +1,6 @@ +framework module Pods_Doughnut { + umbrella header "Pods-Doughnut-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut.release.xcconfig b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut.release.xcconfig new file mode 100644 index 0000000..32f23a2 --- /dev/null +++ b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut.release.xcconfig @@ -0,0 +1,12 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CODE_SIGN_IDENTITY = +FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/FeedKit" "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift" "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks' +OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/FeedKit/FeedKit.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift/GRDB.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences/MASPreferences.framework/Headers" +OTHER_LDFLAGS = $(inherited) -framework "FeedKit" -framework "GRDB" -framework "MASPreferences" +OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" +PODS_BUILD_DIR = $BUILD_DIR +PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut.test.xcconfig b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut.test.xcconfig new file mode 100644 index 0000000..32f23a2 --- /dev/null +++ b/Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut.test.xcconfig @@ -0,0 +1,12 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CODE_SIGN_IDENTITY = +FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/FeedKit" "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift" "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks' +OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/FeedKit/FeedKit.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift/GRDB.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences/MASPreferences.framework/Headers" +OTHER_LDFLAGS = $(inherited) -framework "FeedKit" -framework "GRDB" -framework "MASPreferences" +OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" +PODS_BUILD_DIR = $BUILD_DIR +PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/Pods/Target Support Files/Pods-DoughnutTests/Info.plist b/Pods/Target Support Files/Pods-DoughnutTests/Info.plist new file mode 100644 index 0000000..2243fe6 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutTests/Info.plist @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleShortVersionString</key> + <string>1.0.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>${CURRENT_PROJECT_VERSION}</string> + <key>NSPrincipalClass</key> + <string></string> +</dict> +</plist> diff --git a/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-acknowledgements.markdown b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-acknowledgements.markdown new file mode 100644 index 0000000..102af75 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-acknowledgements.markdown @@ -0,0 +1,3 @@ +# Acknowledgements +This application makes use of the following third party libraries: +Generated by CocoaPods - https://cocoapods.org diff --git a/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-acknowledgements.plist b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-acknowledgements.plist new file mode 100644 index 0000000..7acbad1 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-acknowledgements.plist @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>PreferenceSpecifiers</key> + <array> + <dict> + <key>FooterText</key> + <string>This application makes use of the following third party libraries:</string> + <key>Title</key> + <string>Acknowledgements</string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + <dict> + <key>FooterText</key> + <string>Generated by CocoaPods - https://cocoapods.org</string> + <key>Title</key> + <string></string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + </array> + <key>StringsTable</key> + <string>Acknowledgements</string> + <key>Title</key> + <string>Acknowledgements</string> +</dict> +</plist> diff --git a/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-dummy.m b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-dummy.m new file mode 100644 index 0000000..0fc5876 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-dummy.m @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> +@interface PodsDummy_Pods_DoughnutTests : NSObject +@end +@implementation PodsDummy_Pods_DoughnutTests +@end diff --git a/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-frameworks.sh b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-frameworks.sh new file mode 100755 index 0000000..88dd537 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-frameworks.sh @@ -0,0 +1,105 @@ +#!/bin/sh +set -e + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink "${source}")" + fi + + # Use filter instead of exclude so missing patterns don't throw errors. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} + +# Copies the dSYM of a vendored framework +install_dsym() { + local source="$1" + if [ -r "$source" ]; then + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DWARF_DSYM_FOLDER_PATH}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DWARF_DSYM_FOLDER_PATH}" + fi +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identitiy + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements '$1'" + + if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + code_sign_cmd="$code_sign_cmd &" + fi + echo "$code_sign_cmd" + eval "$code_sign_cmd" + fi +} + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + # Get architectures for current file + archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | rev)" + stripped="" + for arch in $archs; do + if ! [[ "${ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" || exit 1 + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi +} + +if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + wait +fi diff --git a/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-resources.sh b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-resources.sh new file mode 100755 index 0000000..a7df440 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-resources.sh @@ -0,0 +1,106 @@ +#!/bin/sh +set -e + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + +RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt +> "$RESOURCES_TO_COPY" + +XCASSET_FILES=() + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +case "${TARGETED_DEVICE_FAMILY}" in + 1,2) + TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" + ;; + 1) + TARGET_DEVICE_ARGS="--target-device iphone" + ;; + 2) + TARGET_DEVICE_ARGS="--target-device ipad" + ;; + 3) + TARGET_DEVICE_ARGS="--target-device tv" + ;; + 4) + TARGET_DEVICE_ARGS="--target-device watch" + ;; + *) + TARGET_DEVICE_ARGS="--target-device mac" + ;; +esac + +install_resource() +{ + if [[ "$1" = /* ]] ; then + RESOURCE_PATH="$1" + else + RESOURCE_PATH="${PODS_ROOT}/$1" + fi + if [[ ! -e "$RESOURCE_PATH" ]] ; then + cat << EOM +error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. +EOM + exit 1 + fi + case $RESOURCE_PATH in + *.storyboard) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.xib) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.framework) + echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true + mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + ;; + *.xcdatamodel) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" || true + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" + ;; + *.xcdatamodeld) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" || true + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" + ;; + *.xcmappingmodel) + echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" || true + xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" + ;; + *.xcassets) + ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" + XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") + ;; + *) + echo "$RESOURCE_PATH" || true + echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" + ;; + esac +} + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then + mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi +rm -f "$RESOURCES_TO_COPY" + +if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ] +then + # Find all other xcassets (this unfortunately includes those of path pods and other targets). + OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) + while read line; do + if [[ $line != "${PODS_ROOT}*" ]]; then + XCASSET_FILES+=("$line") + fi + done <<<"$OTHER_XCASSETS" + + printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi diff --git a/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-umbrella.h b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-umbrella.h new file mode 100644 index 0000000..17d22f9 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import <Cocoa/Cocoa.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_DoughnutTestsVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_DoughnutTestsVersionString[]; + diff --git a/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests.debug.xcconfig b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests.debug.xcconfig new file mode 100644 index 0000000..75b34c2 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests.debug.xcconfig @@ -0,0 +1,9 @@ +CODE_SIGN_IDENTITY = +FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/FeedKit" "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift" "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/../Frameworks' +OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/FeedKit/FeedKit.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift/GRDB.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences/MASPreferences.framework/Headers" +PODS_BUILD_DIR = $BUILD_DIR +PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests.modulemap b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests.modulemap new file mode 100644 index 0000000..6532444 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests.modulemap @@ -0,0 +1,6 @@ +framework module Pods_DoughnutTests { + umbrella header "Pods-DoughnutTests-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests.release.xcconfig b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests.release.xcconfig new file mode 100644 index 0000000..75b34c2 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests.release.xcconfig @@ -0,0 +1,9 @@ +CODE_SIGN_IDENTITY = +FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/FeedKit" "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift" "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/../Frameworks' +OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/FeedKit/FeedKit.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift/GRDB.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences/MASPreferences.framework/Headers" +PODS_BUILD_DIR = $BUILD_DIR +PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests.test.xcconfig b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests.test.xcconfig new file mode 100644 index 0000000..75b34c2 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutTests/Pods-DoughnutTests.test.xcconfig @@ -0,0 +1,9 @@ +CODE_SIGN_IDENTITY = +FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/FeedKit" "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift" "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/../Frameworks' +OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/FeedKit/FeedKit.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift/GRDB.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences/MASPreferences.framework/Headers" +PODS_BUILD_DIR = $BUILD_DIR +PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/Pods/Target Support Files/Pods-DoughnutUITests/Info.plist b/Pods/Target Support Files/Pods-DoughnutUITests/Info.plist new file mode 100644 index 0000000..2243fe6 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutUITests/Info.plist @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleShortVersionString</key> + <string>1.0.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>${CURRENT_PROJECT_VERSION}</string> + <key>NSPrincipalClass</key> + <string></string> +</dict> +</plist> diff --git a/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-acknowledgements.markdown b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-acknowledgements.markdown new file mode 100644 index 0000000..102af75 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-acknowledgements.markdown @@ -0,0 +1,3 @@ +# Acknowledgements +This application makes use of the following third party libraries: +Generated by CocoaPods - https://cocoapods.org diff --git a/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-acknowledgements.plist b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-acknowledgements.plist new file mode 100644 index 0000000..7acbad1 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-acknowledgements.plist @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>PreferenceSpecifiers</key> + <array> + <dict> + <key>FooterText</key> + <string>This application makes use of the following third party libraries:</string> + <key>Title</key> + <string>Acknowledgements</string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + <dict> + <key>FooterText</key> + <string>Generated by CocoaPods - https://cocoapods.org</string> + <key>Title</key> + <string></string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + </array> + <key>StringsTable</key> + <string>Acknowledgements</string> + <key>Title</key> + <string>Acknowledgements</string> +</dict> +</plist> diff --git a/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-dummy.m b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-dummy.m new file mode 100644 index 0000000..2dc832b --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-dummy.m @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> +@interface PodsDummy_Pods_DoughnutUITests : NSObject +@end +@implementation PodsDummy_Pods_DoughnutUITests +@end diff --git a/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-frameworks.sh b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-frameworks.sh new file mode 100755 index 0000000..88dd537 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-frameworks.sh @@ -0,0 +1,105 @@ +#!/bin/sh +set -e + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink "${source}")" + fi + + # Use filter instead of exclude so missing patterns don't throw errors. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} + +# Copies the dSYM of a vendored framework +install_dsym() { + local source="$1" + if [ -r "$source" ]; then + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DWARF_DSYM_FOLDER_PATH}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DWARF_DSYM_FOLDER_PATH}" + fi +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identitiy + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements '$1'" + + if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + code_sign_cmd="$code_sign_cmd &" + fi + echo "$code_sign_cmd" + eval "$code_sign_cmd" + fi +} + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + # Get architectures for current file + archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | rev)" + stripped="" + for arch in $archs; do + if ! [[ "${ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" || exit 1 + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi +} + +if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + wait +fi diff --git a/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-resources.sh b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-resources.sh new file mode 100755 index 0000000..a7df440 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-resources.sh @@ -0,0 +1,106 @@ +#!/bin/sh +set -e + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + +RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt +> "$RESOURCES_TO_COPY" + +XCASSET_FILES=() + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +case "${TARGETED_DEVICE_FAMILY}" in + 1,2) + TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" + ;; + 1) + TARGET_DEVICE_ARGS="--target-device iphone" + ;; + 2) + TARGET_DEVICE_ARGS="--target-device ipad" + ;; + 3) + TARGET_DEVICE_ARGS="--target-device tv" + ;; + 4) + TARGET_DEVICE_ARGS="--target-device watch" + ;; + *) + TARGET_DEVICE_ARGS="--target-device mac" + ;; +esac + +install_resource() +{ + if [[ "$1" = /* ]] ; then + RESOURCE_PATH="$1" + else + RESOURCE_PATH="${PODS_ROOT}/$1" + fi + if [[ ! -e "$RESOURCE_PATH" ]] ; then + cat << EOM +error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. +EOM + exit 1 + fi + case $RESOURCE_PATH in + *.storyboard) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.xib) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.framework) + echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true + mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + ;; + *.xcdatamodel) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" || true + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" + ;; + *.xcdatamodeld) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" || true + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" + ;; + *.xcmappingmodel) + echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" || true + xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" + ;; + *.xcassets) + ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" + XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") + ;; + *) + echo "$RESOURCE_PATH" || true + echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" + ;; + esac +} + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then + mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi +rm -f "$RESOURCES_TO_COPY" + +if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ] +then + # Find all other xcassets (this unfortunately includes those of path pods and other targets). + OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) + while read line; do + if [[ $line != "${PODS_ROOT}*" ]]; then + XCASSET_FILES+=("$line") + fi + done <<<"$OTHER_XCASSETS" + + printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi diff --git a/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-umbrella.h b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-umbrella.h new file mode 100644 index 0000000..cbfbe3a --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import <Cocoa/Cocoa.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_DoughnutUITestsVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_DoughnutUITestsVersionString[]; + diff --git a/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests.debug.xcconfig b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests.debug.xcconfig new file mode 100644 index 0000000..75b34c2 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests.debug.xcconfig @@ -0,0 +1,9 @@ +CODE_SIGN_IDENTITY = +FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/FeedKit" "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift" "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/../Frameworks' +OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/FeedKit/FeedKit.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift/GRDB.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences/MASPreferences.framework/Headers" +PODS_BUILD_DIR = $BUILD_DIR +PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests.modulemap b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests.modulemap new file mode 100644 index 0000000..a5cee95 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests.modulemap @@ -0,0 +1,6 @@ +framework module Pods_DoughnutUITests { + umbrella header "Pods-DoughnutUITests-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests.release.xcconfig b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests.release.xcconfig new file mode 100644 index 0000000..75b34c2 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests.release.xcconfig @@ -0,0 +1,9 @@ +CODE_SIGN_IDENTITY = +FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/FeedKit" "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift" "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/../Frameworks' +OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/FeedKit/FeedKit.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift/GRDB.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences/MASPreferences.framework/Headers" +PODS_BUILD_DIR = $BUILD_DIR +PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests.test.xcconfig b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests.test.xcconfig new file mode 100644 index 0000000..75b34c2 --- /dev/null +++ b/Pods/Target Support Files/Pods-DoughnutUITests/Pods-DoughnutUITests.test.xcconfig @@ -0,0 +1,9 @@ +CODE_SIGN_IDENTITY = +FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/FeedKit" "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift" "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/../Frameworks' +OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/FeedKit/FeedKit.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/GRDB.swift/GRDB.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/MASPreferences/MASPreferences.framework/Headers" +PODS_BUILD_DIR = $BUILD_DIR +PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/README.md b/README.md new file mode 100644 index 0000000..142830a --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +<h1 align="center"> + <img src="https://github.com/CD1212/Doughnut/raw/master/src/assets/icon.png" alt="Doughnut" width="200"> + <br> + Doughnut + <br> +</h1> + +<h4 align="center">Podcast app. For Mac.</h4> + +<p align="center"> + <a href="https://github.com/CD1212/Doughnut/releases"><img src="https://img.shields.io/github/release/cd1212/doughnut.svg" alt="github release"></a> +</p> + +<p align="center"> + <img src="https://raw.githubusercontent.com/CD1212/Doughnut/master/screenshot.jpg?v=5" align="center" alt="screenshot" style="max-width:100%;" /> +</p> + +Doughnut is a podcast client built using Swift. The design and user experience are inspired by Instacast for Mac which was discontinued in 2015. After experimenting with alternate user interface layouts, I kept coming back to the three column layout as most useable and practical. + +Beyond the standard expected podcast app features, my goals for the project are: +- [x] Support an iTunes style library that can be hosted on an internal or network shared drive +- [x] Ability to favourite episodes +- [x] Ability to create podcasts without a feed, for miscellaneous releases of discontinued podcasts + +Previously Doughnut was built on top of Electron which worked ok, but using 200+ MB for a podcast app, even when it's minimized felt very poor. This version is archived in the `electron` branch. Doughnut is now written as a 100% native MacOS app in Swift. + +## How to Contribute + +### Get the code +``` +$ git clone git@github.com:CD1212/Doughnut.git +$ cd Doughnut +$ open Doughnut.xcworkspace +``` \ No newline at end of file diff --git a/screenshot.jpg b/screenshot.jpg new file mode 100644 index 0000000..c63e9c1 Binary files /dev/null and b/screenshot.jpg differ