├── .github └── FUNDING.yml ├── .gitignore ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── xcshareddata │ └── xcschemes │ ├── QuoteKit.xcscheme │ └── QuoteKitTests.xcscheme ├── Package.resolved ├── Package.swift ├── QuoteKit_Logo.png ├── README.md ├── Sources └── QuoteKit │ ├── APIs │ ├── AuthorAPIs.swift │ ├── QuoteAPIs.swift │ ├── SearchAPIs.swift │ └── TagAPIs.swift │ ├── Endpoint │ ├── QuotableEndpoint.swift │ ├── QuotableEndpointPath.swift │ ├── QuotableURLHost.swift │ └── URLQueryItem.swift │ ├── Extension │ ├── InsecureSessionDelegate.swift │ └── QuoteFetchError.swift │ ├── Model │ ├── Author.swift │ ├── Authors.swift │ ├── AuthorsAndTagsSortType.swift │ ├── QuotableListOrder.swift │ ├── Quote.swift │ ├── QuoteItemCollection.swift │ ├── Quotes.swift │ ├── QuotesSortType.swift │ ├── Tags.swift │ └── URLQueryItemListType.swift │ └── QuoteKit.swift ├── Tests └── QuoteKitTests │ ├── Authors │ ├── AuthorsDataTests.swift │ └── AuthorsURLTests.swift │ ├── QuotableURLHost.swift │ ├── Quotes │ ├── QuotesDataTests.swift │ ├── QuotesURLTests.swift │ └── RandomQuoteURLTests.swift │ └── Tags │ ├── QuoteKitTagsDataTests.swift │ └── QuoteKitTagsURLTests.swift ├── codemagic.yaml └── releases └── v2.0.0-release-notes.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: rryam 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Plugins 3 | - Source 4 | - Tests 5 | excluded: 6 | - Tests/SwiftLintFrameworkTests/Resources 7 | analyzer_rules: 8 | - unused_declaration 9 | - unused_import 10 | opt_in_rules: 11 | - array_init 12 | - attributes 13 | - closure_end_indentation 14 | - closure_spacing 15 | - collection_alignment 16 | - contains_over_filter_count 17 | - contains_over_filter_is_empty 18 | - contains_over_first_not_nil 19 | - contains_over_range_nil_comparison 20 | - discouraged_none_name 21 | - discouraged_object_literal 22 | - empty_collection_literal 23 | - empty_count 24 | - empty_string 25 | - empty_xctest_method 26 | - enum_case_associated_values_count 27 | - explicit_init 28 | - extension_access_modifier 29 | - fallthrough 30 | - fatal_error_message 31 | - file_header 32 | - file_name 33 | - first_where 34 | - flatmap_over_map_reduce 35 | - identical_operands 36 | - joined_default_parameter 37 | - last_where 38 | - legacy_multiple 39 | - literal_expression_end_indentation 40 | - local_doc_comment 41 | - lower_acl_than_parent 42 | - modifier_order 43 | - nimble_operator 44 | - nslocalizedstring_key 45 | - number_separator 46 | - object_literal 47 | - operator_usage_whitespace 48 | - overridden_super_call 49 | - override_in_extension 50 | - pattern_matching_keywords 51 | - prefer_self_type_over_type_of_self 52 | - private_action 53 | - private_outlet 54 | - prohibited_interface_builder 55 | - prohibited_super_call 56 | - quick_discouraged_call 57 | - quick_discouraged_focused_test 58 | - quick_discouraged_pending_test 59 | - reduce_into 60 | - redundant_nil_coalescing 61 | - redundant_type_annotation 62 | - return_value_from_void_function 63 | - single_test_class 64 | - sorted_first_last 65 | - sorted_imports 66 | - static_operator 67 | - strong_iboutlet 68 | - test_case_accessibility 69 | - toggle_bool 70 | - unavailable_function 71 | - unneeded_parentheses_in_closure_argument 72 | - unowned_variable_capture 73 | - untyped_error_in_catch 74 | - vertical_parameter_alignment_on_call 75 | - vertical_whitespace_closing_braces 76 | - vertical_whitespace_opening_braces 77 | - xct_specific_matcher 78 | - yoda_condition 79 | 80 | identifier_name: 81 | excluded: 82 | - id 83 | large_tuple: 3 84 | number_separator: 85 | minimum_length: 5 86 | file_name: 87 | excluded: 88 | - SwiftSyntax+SwiftLint.swift 89 | - GeneratedTests.swift 90 | - TestHelpers.swift 91 | 92 | function_body_length: 60 93 | type_body_length: 400 94 | 95 | custom_rules: 96 | rule_id: 97 | included: Source/SwiftLintFramework/Rules/.+/\w+\.swift 98 | name: Rule ID 99 | message: Rule IDs must be all lowercase, snake case and not end with `rule` 100 | regex: identifier:\s*("\w+_rule"|"\S*[^a-z_]\S*") 101 | severity: error 102 | fatal_error: 103 | name: Fatal Error 104 | excluded: "Tests/*" 105 | message: Prefer using `queuedFatalError` over `fatalError` to avoid leaking compiler host machine paths. 106 | regex: \bfatalError\b 107 | match_kinds: 108 | - identifier 109 | severity: error 110 | rule_test_function: 111 | included: Tests/SwiftLintFrameworkTests/RulesTests.swift 112 | name: Rule Test Function 113 | message: Rule Test Function mustn't end with `rule` 114 | regex: func\s*test\w+(r|R)ule\(\) 115 | severity: error 116 | 117 | unused_import: 118 | always_keep_imports: 119 | - SwiftSyntaxBuilder # we can't detect uses of string interpolation of swift syntax nodes 120 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/QuoteKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/QuoteKitTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 37 | 38 | 44 | 45 | 47 | 48 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "SwiftDocCPlugin", 6 | "repositoryURL": "https://github.com/apple/swift-docc-plugin", 7 | "state": { 8 | "branch": null, 9 | "revision": "10bc670db657d11bdd561e07de30a9041311b2b1", 10 | "version": "1.1.0" 11 | } 12 | }, 13 | { 14 | "package": "SymbolKit", 15 | "repositoryURL": "https://github.com/apple/swift-docc-symbolkit", 16 | "state": { 17 | "branch": null, 18 | "revision": "b45d1f2ed151d057b54504d653e0da5552844e34", 19 | "version": "1.0.0" 20 | } 21 | }, 22 | { 23 | "package": "SwiftLintPlugin", 24 | "repositoryURL": "https://github.com/lukepistrol/SwiftLintPlugin", 25 | "state": { 26 | "branch": null, 27 | "revision": "f69b412a765396d44dc9f4788a5b79919c1ca9e3", 28 | "version": "0.2.2" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "QuoteKit", 8 | platforms: [.iOS(.v15), .macOS(.v12), .tvOS(.v15), .watchOS(.v8)], 9 | products: [.library(name: "QuoteKit", targets: ["QuoteKit"])], 10 | dependencies: [ 11 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), 12 | .package(url: "https://github.com/lukepistrol/SwiftLintPlugin", from: "0.2.2"), 13 | ], 14 | targets: [ 15 | .target( 16 | name: "QuoteKit", dependencies: [], 17 | plugins: [.plugin(name: "SwiftLint", package: "SwiftLintPlugin")]), 18 | .testTarget(name: "QuoteKitTests", dependencies: ["QuoteKit"]), 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /QuoteKit_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rryam/QuoteKit/83218a37670a4cabe8a5fbde41e0e9e29444d239/QuoteKit_Logo.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | QuoteKit Logo 3 |

4 | 5 | # QuoteKit 6 | 7 | ![Twitter Follow](https://img.shields.io/twitter/follow/rudrankriyam?style=social) 8 | 9 | The QuoteKit is a Swift framework to use the free APIs provided by [Quotable](https://github.com/lukePeavey/quotable) created by [Luke Peavey](https://github.com/lukePeavey). It uses the latest async/await syntax for easy access and contains all the APIs like fetching a random quote, all quotes, authors, tags, and searching quotes and authors. 10 | 11 | ## Support 12 | 13 | Love this project? Check out my books to explore more of AI and iOS development: 14 | - [Exploring AI for iOS Development](https://academy.rudrank.com/product/ai) 15 | - [Exploring AI-Assisted Coding for iOS Development](https://academy.rudrank.com/product/ai-assisted-coding) 16 | 17 | Your support helps to keep this project growing! 18 | 19 | - [Requirements](#requirements) 20 | - [Installation](#installation) 21 | - [Backup API Support](#backup-api-support) 22 | - [Usage](#usage) 23 | - [Random Quote](#random-quote) 24 | - [List Quotes](#list-quotes) 25 | - [Quote By ID](#quote-by-id) 26 | - [List Authors](#list-authors) 27 | - [Author By ID](#author-by-id) 28 | - [Author Profile Image URL](#author-profile-image-url) 29 | - [List Tags](#list-tags) 30 | - [Search Quotes](#search-quotes) 31 | - [Search Authors](#search-authors) 32 | - [Data Models](#data-models) 33 | 34 | ## Requirements 35 | 36 | As it uses the async/await feature of Swift 5.5, the platforms supported are iOS 15.0+, macOS 12.0+, watchOS 8.0+, and tvOS 15.0+. 37 | 38 | ## Installation 39 | 40 | The best way to add QuoteKit to your project is via the Swift Package Manager. 41 | 42 | ``` 43 | dependencies: [ 44 | .package(url: "https://github.com/rudrankriyam/QuoteKit.git") 45 | ] 46 | ``` 47 | 48 | ## Installation 49 | 50 | The best way to add QuoteKit to your project is via the Swift Package Manager. 51 | 52 | ``` 53 | dependencies: [ 54 | .package(url: "https://github.com/rudrankriyam/QuoteKit.git") 55 | ] 56 | ``` 57 | 58 | -> 59 | 60 | ## Installation 61 | 62 | The best way to add QuoteKit to your project is via the Swift Package Manager. 63 | 64 | ``` 65 | dependencies: [ 66 | .package(url: "https://github.com/rudrankriyam/QuoteKit.git") 67 | ] 68 | ``` 69 | 70 | ## Backup API Support 71 | 72 | ⚠️ **Important**: Due to SSL certificate issues with the original Quotable API servers, QuoteKit now includes backup API support to ensure reliable service. 73 | 74 | ### Current API Status 75 | - **Primary APIs**: `api.quotable.io` and `staging.quotable.io` have expired SSL certificates 76 | - **Backup API**: `api.quotable.kurokeita.dev` is fully functional and compatible 77 | 78 | ### Using the Backup API 79 | 80 | #### Option 1: Enable Backup API (Recommended) 81 | Set the environment variable to use the working backup API: 82 | 83 | ```bash 84 | export QUOTEKIT_USE_BACKUP=1 85 | ``` 86 | 87 | #### Option 2: Automatic Fallback 88 | QuoteKit automatically falls back to the backup API if the primary API fails. Your existing code continues to work without changes. 89 | 90 | #### Option 3: SSL Bypass for Testing 91 | For testing purposes only, you can bypass SSL verification: 92 | 93 | ```bash 94 | export QUOTEKIT_INSECURE_SSL=1 95 | ``` 96 | 97 | ### Code Examples with Backup API 98 | 99 | ```swift 100 | // Your existing code works unchanged 101 | let quote = try await QuoteKit.randomQuote() 102 | print(quote.content) 103 | 104 | // QuoteKit automatically handles: 105 | // - API endpoint switching 106 | // - Response format conversion 107 | // - SSL certificate issues 108 | // - Fallback mechanisms 109 | ``` 110 | 111 | ## Usage 112 | 113 | The `struct QuoteKit` contains static methods to fetch the relevant data. For example, to get the list of quotes - 114 | 115 | ```swift 116 | do { 117 | var quotes: Quotes? 118 | quotes = try await QuoteKit.quotes() 119 | } catch { 120 | print(error) 121 | } 122 | ``` 123 | 124 | The examples given below are similar to Quotable's [README.](https://github.com/lukePeavey/quotable/blob/master/README.md) 125 | 126 | ## Random Quote 127 | 128 | Returns a single random `Quote` object from the `/random` API. 129 | 130 | ```swift 131 | var randomQuote: Quote? 132 | randomQuote = try await QuoteKit.randomQuote() 133 | ``` 134 | 135 | You can customize the request by adding query parameters like the minimum and maximum length of the quote or its tag. You can also get a random quote from a specific author(s). 136 | 137 | Few examples: 138 | 139 | Random Quote with tags "technology" AND "famous-quotes" - 140 | 141 | ```swift 142 | try await QuoteKit.randomQuote(tags: [.technology, .famousQuotes], type: .all) 143 | ``` 144 | 145 | Random Quote with tags "History" OR "Civil Rights" - 146 | 147 | ```swift 148 | try await QuoteKit.randomQuote(tags: [.history, .civilRights], type: .either) 149 | ``` 150 | 151 | Random Quote with a maximum length of 50 characters - 152 | 153 | ```swift 154 | try await QuoteKit.randomQuote(maxLength: 150) 155 | ``` 156 | 157 | Random Quote with a length between 100 and 140 characters - 158 | 159 | ```swift 160 | try await QuoteKit.randomQuote(minLength: 100, maxLength: 140) 161 | ``` 162 | 163 | Random Quote by the author "Aesop" and "Stephen Hawking" - 164 | 165 | ```swift 166 | try await QuoteKit.randomQuote(authors: ["aesop", "stephen-hawking"]) 167 | ``` 168 | 169 | ## List Quotes 170 | 171 | Returns the `Quotes` object based on the given queries from the `/quotes` API. By default, the list contains 20 `Quote` on one page. 172 | 173 | ```swift 174 | var quotes: Quotes? 175 | quotes = try await QuoteKit.quotes() 176 | ``` 177 | 178 | Few examples: 179 | 180 | Get all quotes with a maximum length of 50 characters - 181 | 182 | ```swift 183 | try await QuoteKit.quotes(maxLength: 150) 184 | ``` 185 | 186 | Get all quotes with a length between 100 and 140 characters - 187 | 188 | ```swift 189 | try await QuoteKit.quotes(minLength: 100, maxLength: 140) 190 | ``` 191 | 192 | Get the first page of quotes, with 20 results per page - 193 | 194 | ```swift 195 | try await QuoteKit.quotes(page: 1) 196 | ``` 197 | 198 | Get the second page of quotes, with 20 results per page, with a limit of 10 quotes - 199 | 200 | ```swift 201 | try await QuoteKit.quotes(limit: 10, page: 2) 202 | ``` 203 | 204 | Get all quotes with the tags love OR happiness - 205 | 206 | ```swift 207 | try await QuoteKit.quotes(tags: [.love, .happiness], type: .either) 208 | ``` 209 | 210 | Get all quotes with the tags technology AND famous-quotes - 211 | 212 | ```swift 213 | try await QuoteKit.quotes(tags: [.technology, .famousQuotes], type: .all) 214 | ``` 215 | 216 | Get all quotes by author, using the author's slug - 217 | 218 | ```swift 219 | try await QuoteKit.quotes(authors: ["albert-einstein"]) 220 | ``` 221 | 222 | Get all quotes sorted by the author - 223 | 224 | ```swift 225 | try await QuoteKit.quotes(sortBy: .author) 226 | ``` 227 | 228 | Get all quotes sorted by content, in descending order - 229 | 230 | ```swift 231 | try await QuoteKit.quotes(sortBy: .content, order: .descending) 232 | ``` 233 | 234 | ## Quote By ID 235 | 236 | If there is one, return a single `Quote` object for the given id from the `/quotes/:id` API. 237 | 238 | ```swift 239 | var quote: Quote? 240 | quote = try await QuoteKit.quote(id: "2xpHvSOQMD") 241 | ``` 242 | 243 | ## List Authors 244 | 245 | Returns the `Authors` object matching the given queries from the `/authors` API. By default, the list contains 20 `Author` on one page. You can filter multiple authors by providing their slugs in the query parameter. 246 | 247 | ```swift 248 | var authors: Authors? 249 | authors = try await QuoteKit.authors() 250 | ``` 251 | 252 | Few examples: 253 | 254 | Get the first page of authors, with 20 results per page - 255 | 256 | ```swift 257 | try await QuoteKit.authors(page: 1) 258 | ``` 259 | 260 | Get the second page of authors, with 20 results per page, with a limit of 10 authors - 261 | 262 | ```swift 263 | try await QuoteKit.authors(limit: 10, page: 2) 264 | ``` 265 | 266 | Get all authors, sorted alphabetically by name - 267 | 268 | ```swift 269 | try await QuoteKit.authors(sortBy: .name) 270 | ``` 271 | 272 | Get all authors sorted by number of quotes in descending order - 273 | 274 | ```swift 275 | try await QuoteKit.authors(sortBy: .quoteCount, order: .descending) 276 | ``` 277 | 278 | Get a single author by slug - 279 | 280 | ```swift 281 | try await QuoteKit.authors(slugs: ["albert-einstein"]) 282 | ``` 283 | 284 | Get multiple authors by slug - 285 | 286 | ```swift 287 | try await QuoteKit.authors(slugs: ["albert-einstein", "abraham-lincoln"]) 288 | ``` 289 | 290 | ## Author By ID 291 | 292 | If there is one, return a single `Author` object for the given id from the `/authors/:id` API. 293 | 294 | ```swift 295 | var author: Author? 296 | author = try await QuoteKit.author(id: "XYxYtSeixS-o") 297 | ``` 298 | 299 | ## Author Profile Image URL 300 | 301 | Returns the image URL for the given author slug. You can specify the image size as well. The default image size is 700x700. 302 | 303 | ```swift 304 | var authorImageURL: URL? 305 | authorImageURL = QuoteKit.authorProfile(size: 1000, slug: "aesop") 306 | ``` 307 | 308 | ## List Tags 309 | 310 | Returns the `Tags` object containing the list of all tags from the `/tags` API. You can sort it and order the sorted results. 311 | 312 | ```swift 313 | var tags: Tags? 314 | tags = try await QuoteKit.tags() 315 | ``` 316 | 317 | Get all tags, sorted alphabetically by name - 318 | 319 | ```swift 320 | try await QuoteKit.tags(sortBy: .name) 321 | ``` 322 | 323 | Get all tags, sorted by number of quotes in descending order - 324 | 325 | ```swift 326 | try await QuoteKit.tags(sortBy: .quoteCount, order: .descending) 327 | ``` 328 | 329 | ## Search Quotes 330 | 331 | Returns the `Quotes` object based on the search query from the `/search/quotes` API. By default, the list contains 20 `Quote` on one page. 332 | 333 | ```swift 334 | var quotes: Quotes? 335 | quotes = try await QuoteKit.searchQuotes(for: "love") 336 | ``` 337 | 338 | Get the first page of searched quotes, with 20 results per page - 339 | 340 | ```swift 341 | try await QuoteKit.searchQuotes(for: "love", page: 1) 342 | ``` 343 | 344 | Get the second page of searched quotes, with 20 results per page, with a limit of 10 quotes - 345 | 346 | ```swift 347 | try await QuoteKit.searchQuotes(for: "love", limit: 10, page: 2) 348 | ``` 349 | 350 | ## Search Authors 351 | 352 | Returns the `Authors` object based on the search query from the `/search/authors` API. By default, the list contains 20 `Author` on one page. 353 | 354 | ```swift 355 | var quotes: Quotes? 356 | quotes = try await QuoteKit.searchAuthors(for: "kalam") 357 | ``` 358 | 359 | Get the first page of searched authors, with 20 results per page - 360 | 361 | ```swift 362 | try await QuoteKit.searchAuthors(for: "kalam", page: 1) 363 | ``` 364 | 365 | Get the second page of searched authors, with 20 results per page, with a limit of 10 authors - 366 | 367 | ```swift 368 | try await QuoteKit.searchAuthors(for: "kalam", limit: 10, page: 2) 369 | ``` 370 | 371 | ## Data Models 372 | 373 | There are many different data models for using this framework. 374 | 375 | - `Quote` 376 | 377 | The object represents a single quote. You can get the content of the quote using the `content` variable. The `tags` is an array of the relevant tag associated with the quote. To get the number of characters in the quote, use `length.` 378 | 379 | ```swift 380 | struct Quote: Decodable, Identifiable { 381 | var id: String 382 | var tags: [String] 383 | var content: String 384 | var author: String 385 | var authorSlug: String 386 | var length: Int 387 | var dateAdded: String 388 | var dateModified: String 389 | 390 | enum CodingKeys: String, CodingKey { 391 | case id = "_id" 392 | case tags, content, author, authorSlug, length, dateAdded, dateModified 393 | } 394 | } 395 | ``` 396 | 397 | - `Author` 398 | 399 | The object represents a single author. You can get the link to their Wikipedia page or their official website using `link.` `bio` contains a brief, one paragraph about the author. Use `description` instead to get a shorter description of the person's occupation or what they're known for. `quotes` contains an array of the author's quote. 400 | 401 | ```swift 402 | struct Author: Decodable, Identifiable { 403 | var id: String 404 | var link: String 405 | var bio: String 406 | var description: String 407 | var name: String 408 | var quoteCount: Int 409 | var slug: String 410 | var dateAdded: String 411 | var dateModified: String 412 | var quotes: [Quote]? 413 | 414 | enum CodingKeys: String, CodingKey { 415 | case link, bio, description 416 | case id = "_id" 417 | case name, quoteCount, slug 418 | case dateAdded, dateModified 419 | case quotes 420 | } 421 | } 422 | 423 | extension Author: Equatable { 424 | static func ==(lhs: Author, rhs: Author) -> Bool { 425 | lhs.id == rhs.id 426 | } 427 | } 428 | ``` 429 | -------------------------------------------------------------------------------- /Sources/QuoteKit/APIs/AuthorAPIs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorAPIs.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 03/10/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension QuoteKit { 11 | static func authorImage(with slug: String, size: Int = 700) -> URL { 12 | QuotableEndpoint(.authorProfile(size, slug), host: .images).url 13 | } 14 | 15 | static func author(id: String) async throws -> Author { 16 | try await execute(with: QuotableEndpoint(.author(id))) 17 | } 18 | 19 | static func authors(slugs: [String]? = nil, 20 | sortBy: AuthorsAndTagsSortType? = nil, 21 | order: QuotableListOrder? = nil, 22 | limit: Int = 20, 23 | page: Int = 1) async throws -> Authors { 24 | 25 | let queryItems = authorsParameters(slugs: slugs, sortBy: sortBy, order: order, limit: limit, page: page) 26 | 27 | return try await execute(with: QuotableEndpoint(.authors, queryItems: queryItems)) 28 | } 29 | 30 | private static func authorsParameters(slugs: [String]? = nil, 31 | sortBy: AuthorsAndTagsSortType? = nil, 32 | order: QuotableListOrder? = nil, 33 | limit: Int = 20, 34 | page: Int = 1) -> [URLQueryItem] { 35 | 36 | var queryItems: [URLQueryItem] = [] 37 | 38 | queryItems.append(.limit(limit)) 39 | queryItems.append(.page(page)) 40 | 41 | if let slugs = slugs { 42 | queryItems.append(.slugs(slugs)) 43 | } 44 | 45 | if let sortBy = sortBy { 46 | queryItems.append(.sortBy(sortBy)) 47 | } 48 | 49 | if let order = order { 50 | queryItems.append(.order(order)) 51 | } 52 | 53 | return queryItems 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/QuoteKit/APIs/QuoteAPIs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuoteAPIs.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 03/10/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension QuoteKit { 11 | static func quote(id: String) async throws -> Quote { 12 | try await execute(with: QuotableEndpoint(.quote(id))) 13 | } 14 | 15 | static func quotes(minLength: Int? = nil, 16 | maxLength: Int? = nil, 17 | tags: [String]? = nil, 18 | type: URLQueryItemListType = .all, 19 | authors: [String]? = nil, 20 | sortBy: QuotesSortType? = nil, 21 | order: QuotableListOrder? = nil, 22 | limit: Int = 20, 23 | page: Int = 1) async throws -> Quotes { 24 | 25 | let queryItems = quotesParameter(minLength: minLength, 26 | maxLength: maxLength, 27 | tags: tags, 28 | type: type, 29 | authors: authors, 30 | sortBy: sortBy, 31 | order: order, 32 | limit: limit, 33 | page: page) 34 | 35 | return try await execute(with: QuotableEndpoint(.quotes, queryItems: queryItems)) 36 | } 37 | 38 | static private func quotesParameter(minLength: Int? = nil, 39 | maxLength: Int? = nil, 40 | tags: [String]? = nil, 41 | type: URLQueryItemListType = .all, 42 | authors: [String]? = nil, 43 | sortBy: QuotesSortType? = nil, 44 | order: QuotableListOrder? = nil, 45 | limit: Int = 20, 46 | page: Int = 1) -> [URLQueryItem] { 47 | 48 | var queryItems: [URLQueryItem] = [] 49 | 50 | queryItems.append(.limit(limit)) 51 | queryItems.append(.page(page)) 52 | 53 | if let minLength = minLength { 54 | queryItems.append(.minLength(minLength)) 55 | } 56 | 57 | if let maxLength = maxLength { 58 | queryItems.append(.maxLength(maxLength)) 59 | } 60 | 61 | if let tags = tags { 62 | queryItems.append(.tags(tags, type)) 63 | } 64 | 65 | if let authors = authors { 66 | queryItems.append(.authors(authors)) 67 | } 68 | 69 | if let sortBy = sortBy { 70 | queryItems.append(.sortQuotesBy(sortBy)) 71 | } 72 | 73 | if let order = order { 74 | queryItems.append(.order(order)) 75 | } 76 | 77 | return queryItems 78 | } 79 | 80 | static func quotes() async throws -> Quotes { 81 | try await execute(with: QuotableEndpoint(.quotes)) 82 | } 83 | 84 | static func randomQuote(minLength: Int? = nil, 85 | maxLength: Int? = nil, 86 | tags: [String]? = nil, 87 | type: URLQueryItemListType = .all, 88 | authors: [String]? = nil) async throws -> Quote { 89 | 90 | let queryItems = randomQuoteParameters(minLength: minLength, 91 | maxLength: maxLength, 92 | tags: tags, 93 | type: type, 94 | authors: authors) 95 | 96 | return try await execute(with: QuotableEndpoint(.randomQuote, queryItems: queryItems)) 97 | } 98 | 99 | static private func randomQuoteParameters(minLength: Int? = nil, 100 | maxLength: Int? = nil, 101 | tags: [String]? = nil, 102 | type: URLQueryItemListType = .all, 103 | authors: [String]? = nil) -> [URLQueryItem] { 104 | 105 | var queryItems: [URLQueryItem] = [] 106 | 107 | if let minLength = minLength { 108 | queryItems.append(.minLength(minLength)) 109 | } 110 | 111 | if let maxLength = maxLength { 112 | queryItems.append(.maxLength(maxLength)) 113 | } 114 | 115 | if let tags = tags { 116 | queryItems.append(.tags(tags, type)) 117 | } 118 | 119 | if let authors = authors { 120 | queryItems.append(.authors(authors)) 121 | } 122 | 123 | return queryItems 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/QuoteKit/APIs/SearchAPIs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 03/10/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension QuoteKit { 11 | static func searchQuotes(for query: String, 12 | limit: Int = 20, 13 | page: Int = 1) async throws -> Quotes { 14 | try await search(path: .searchQuotes, query: query, limit: limit, page: page) 15 | } 16 | 17 | static func searchAuthors(for query: String, 18 | limit: Int = 20, 19 | page: Int = 1) async throws -> Authors { 20 | try await search(path: .searchAuthors, query: query, limit: limit, page: page) 21 | } 22 | 23 | private static func search(path: QuotableEndpointPath, 24 | query: String, 25 | limit: Int = 20, 26 | page: Int = 1) async throws -> Model { 27 | 28 | let queryItems = searchParameters(query: query, limit: limit, page: page) 29 | 30 | return try await execute(with: QuotableEndpoint(path, queryItems: queryItems)) 31 | } 32 | 33 | private static func searchParameters(query: String, 34 | limit: Int = 20, 35 | page: Int = 1) -> [URLQueryItem] { 36 | var queryItems: [URLQueryItem] = [] 37 | 38 | queryItems.append(.search(query)) 39 | queryItems.append(.limit(limit)) 40 | queryItems.append(.page(page)) 41 | 42 | return queryItems 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/QuoteKit/APIs/TagAPIs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 03/10/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension QuoteKit { 11 | static func tags(sortBy: AuthorsAndTagsSortType? = nil, 12 | order: QuotableListOrder? = nil) async throws -> Tags { 13 | 14 | let queryItems = tagsParameters(sortBy: sortBy, order: order) 15 | 16 | return try await execute(with: QuotableEndpoint(.tags, queryItems: queryItems)) 17 | } 18 | 19 | private static func tagsParameters(sortBy: AuthorsAndTagsSortType? = nil, 20 | order: QuotableListOrder? = nil) -> [URLQueryItem] { 21 | 22 | var queryItems: [URLQueryItem] = [] 23 | 24 | if let sortBy = sortBy { 25 | queryItems.append(.sortBy(sortBy)) 26 | } 27 | 28 | if let order = order { 29 | queryItems.append(.order(order)) 30 | } 31 | 32 | return queryItems 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/QuoteKit/Endpoint/QuotableEndpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuotableEndpoint.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 28/08/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A struct that represents an endpoint for the Quotable API. 11 | /// 12 | /// Use this struct to create a URL for a specific endpoint on the Quotable API. 13 | /// 14 | /// Example usage: 15 | /// 16 | /// ``` 17 | /// let endpoint = QuotableEndpoint(.random) 18 | /// let url = endpoint.url 19 | /// ``` 20 | public struct QuotableEndpoint { 21 | 22 | /// The path component of the endpoint. 23 | var path: QuotableEndpointPath 24 | 25 | /// The query items to include in the URL, if any. 26 | var queryItems: [URLQueryItem]? 27 | 28 | /// The host to use for the URL. 29 | var host: QuotableURLHost 30 | 31 | /// Initializes a new `QuotableEndpoint` instance with the given properties. 32 | /// 33 | /// - Parameters: 34 | /// - path: The path component of the endpoint. 35 | /// - queryItems: The query items to include in the URL, if any. 36 | /// - host: The host to use for the URL. 37 | init( 38 | _ path: QuotableEndpointPath, queryItems: [URLQueryItem]? = nil, 39 | host: QuotableURLHost = .default 40 | ) { 41 | self.path = path 42 | self.queryItems = queryItems 43 | self.host = host 44 | } 45 | } 46 | 47 | extension QuotableEndpoint { 48 | 49 | /// The URL for the endpoint. 50 | var url: URL { 51 | var components = URLComponents() 52 | components.scheme = "https" 53 | components.host = host.rawValue 54 | 55 | // Handle different path structures for different hosts 56 | if host.rawValue == "api.quotable.kurokeita.dev" { 57 | // Backup API requires /api/ prefix 58 | components.path = "/api/" + path.description 59 | } else { 60 | // Original APIs use direct path 61 | components.path = "/" + path.description 62 | } 63 | 64 | if let queryItems = queryItems, !queryItems.isEmpty { 65 | components.queryItems = queryItems 66 | } 67 | 68 | guard let url = components.url else { 69 | preconditionFailure("Invalid URL components: \(components)") 70 | } 71 | 72 | return url 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/QuoteKit/Endpoint/QuotableEndpointPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuotableEndpointPath.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 29/08/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An enum that represents the path component for a Quotable API endpoint. 11 | /// 12 | /// Use this enum to specify the path component for a specific endpoint on the Quotable API. 13 | /// 14 | /// Example usage: 15 | /// 16 | /// ``` 17 | /// let path: QuotableEndpointPath = .randomQuote 18 | /// ``` 19 | public enum QuotableEndpointPath: CustomStringConvertible { 20 | 21 | /// The path for the quotes endpoint. 22 | case quotes 23 | 24 | /// The path for the random quote endpoint. 25 | case randomQuote 26 | 27 | /// The path for the authors endpoint. 28 | case authors 29 | 30 | /// The path for the tags endpoint. 31 | case tags 32 | 33 | /// The path for a specific quote, identified by ID. 34 | case quote(String) 35 | 36 | /// The path for a specific author, identified by ID. 37 | case author(String) 38 | 39 | /// The path for an author's profile image, identified by size and slug. 40 | case authorProfile(Int, String) 41 | 42 | /// The path for the search quotes endpoint. 43 | case searchQuotes 44 | 45 | /// The path for the search authors endpoint. 46 | case searchAuthors 47 | 48 | /// A string representation of the path. 49 | public var description: String { 50 | switch self { 51 | case .quotes: return "quotes" 52 | case .randomQuote: return "quotes/random" 53 | case .authors: return "authors" 54 | case .tags: return "tags" 55 | case .quote(let id): return "quotes/\(id)" 56 | case .author(let id): return "authors/\(id)" 57 | case .authorProfile(let size, let slug): return "profile/\(size)/\(slug).jpg" 58 | case .searchQuotes: return "search/quotes" 59 | case .searchAuthors: return "search/authors" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/QuoteKit/Endpoint/QuotableURLHost.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuotableURLHost.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 29/08/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Learnt this way of creating a URLHost from this article - https://www.swiftbysundell.com/articles/testing-networking-logic-in-swift/ 11 | 12 | struct QuotableURLHost: RawRepresentable { 13 | var rawValue: String 14 | } 15 | 16 | extension QuotableURLHost { 17 | static var staging: Self { 18 | QuotableURLHost(rawValue: "staging.quotable.io") 19 | } 20 | 21 | static var images: Self { 22 | QuotableURLHost(rawValue: "images.quotable.dev") 23 | } 24 | 25 | static var production: Self { 26 | QuotableURLHost(rawValue: "api.quotable.io") 27 | } 28 | 29 | /// Backup API host that works when the primary APIs have SSL certificate issues 30 | static var backup: Self { 31 | QuotableURLHost(rawValue: "api.quotable.kurokeita.dev") 32 | } 33 | 34 | static var `default`: Self { 35 | // Check environment variable to use backup API 36 | if ProcessInfo.processInfo.environment["QUOTEKIT_USE_BACKUP"] == "1" { 37 | return backup 38 | } 39 | 40 | #if DEBUG 41 | return staging 42 | #else 43 | return production 44 | #endif 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/QuoteKit/Endpoint/URLQueryItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLQueryItem.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 29/08/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension URLQueryItem { 11 | static func maxLength(_ length: Int) -> Self { 12 | URLQueryItem(name: "maxLength", value: String(length)) 13 | } 14 | 15 | static func minLength(_ length: Int) -> Self { 16 | URLQueryItem(name: "minLength", value: String(length)) 17 | } 18 | 19 | static func tags(_ tags: [String], _ type: URLQueryItemListType = .all) -> Self { 20 | var tagsValue = "" 21 | 22 | switch type { 23 | case .all: 24 | tagsValue = tags.map { $0 }.joined(separator: ",") 25 | case .either: 26 | tagsValue = tags.map { $0 }.joined(separator: "|") 27 | } 28 | 29 | return URLQueryItem(name: "tags", value: tagsValue) 30 | } 31 | 32 | static func authors(_ authors: [String]) -> Self { 33 | let authorsValue = authors.joined(separator: "|") 34 | return URLQueryItem(name: "author", value: authorsValue) 35 | } 36 | 37 | static func slugs(_ slugs: [String]) -> Self { 38 | let slugsValue = slugs.joined(separator: "|") 39 | return URLQueryItem(name: "slug", value: slugsValue) 40 | } 41 | 42 | static func limit(_ limit: Int) -> Self { 43 | if limit < 1 || limit > 150 { 44 | return URLQueryItem(name: "limit", value: String(20)) 45 | } else { 46 | return URLQueryItem(name: "limit", value: String(limit)) 47 | } 48 | } 49 | 50 | static func page(_ page: Int) -> Self { 51 | if page < 1 { 52 | return URLQueryItem(name: "page", value: String(1)) 53 | } else { 54 | return URLQueryItem(name: "page", value: String(page)) 55 | } 56 | } 57 | 58 | static func sortQuotesBy(_ sortType: QuotesSortType) -> Self { 59 | URLQueryItem(name: "sortBy", value: sortType.rawValue) 60 | } 61 | 62 | static func sortBy(_ sortType: AuthorsAndTagsSortType) -> Self { 63 | URLQueryItem(name: "sortBy", value: sortType.rawValue) 64 | } 65 | 66 | static func order(_ order: QuotableListOrder) -> Self { 67 | URLQueryItem(name: "order", value: order.rawValue) 68 | } 69 | 70 | static func search(_ query: String) -> Self { 71 | URLQueryItem(name: "query", value: query) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/QuoteKit/Extension/InsecureSessionDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InsecureSessionDelegate.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 26/05/25. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Custom delegate to bypass SSL validation (for testing only) 11 | /// This delegate disables SSL certificate validation and should NEVER be used in production. 12 | final class InsecureSessionDelegate: NSObject, URLSessionDelegate { 13 | 14 | func urlSession( 15 | _ session: URLSession, 16 | didReceive challenge: URLAuthenticationChallenge 17 | ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { 18 | // For an insecure session, always trust the server certificate. 19 | // This is dangerous and should only be used for local testing with known servers. 20 | guard let serverTrust = challenge.protectionSpace.serverTrust else { 21 | // If there's no serverTrust, fall back to default handling 22 | return (.performDefaultHandling, nil) 23 | } 24 | 25 | // Create credential from server trust and use it 26 | let credential = URLCredential(trust: serverTrust) 27 | return (.useCredential, credential) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/QuoteKit/Extension/QuoteFetchError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuoteFetchError.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 31/10/21. 6 | // 7 | 8 | import Foundation 9 | 10 | enum QuoteFetchError: Error { 11 | case invalidURL 12 | case missingData 13 | case invalidResponse 14 | } 15 | -------------------------------------------------------------------------------- /Sources/QuoteKit/Model/Author.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Author.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 28/08/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Author: Identifiable, Sendable { 11 | public var id: String 12 | public var link: String 13 | public var bio: String 14 | public var description: String 15 | public var name: String 16 | public var quoteCount: Int 17 | public var slug: String 18 | public var dateAdded: String 19 | public var dateModified: String 20 | public var quotes: [Quote]? 21 | 22 | public init(id: String, link: String, bio: String, description: String, name: String, quoteCount: Int, slug: String, dateAdded: String, dateModified: String, quotes: [Quote]? = nil) { 23 | self.id = id 24 | self.link = link 25 | self.bio = bio 26 | self.description = description 27 | self.name = name 28 | self.quoteCount = quoteCount 29 | self.slug = slug 30 | self.dateAdded = dateAdded 31 | self.dateModified = dateModified 32 | self.quotes = quotes 33 | } 34 | } 35 | 36 | extension Author: Decodable { 37 | enum CodingKeys: String, CodingKey { 38 | case link, bio, description 39 | case id = "_id" 40 | case name, quoteCount, slug 41 | case dateAdded, dateModified 42 | case quotes 43 | } 44 | } 45 | 46 | extension Author: Equatable { 47 | public static func ==(lhs: Author, rhs: Author) -> Bool { 48 | lhs.id == rhs.id 49 | } 50 | } 51 | 52 | extension Author: Hashable { 53 | public func hash(into hasher: inout Hasher) { 54 | hasher.combine(id) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/QuoteKit/Model/Authors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Authors.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 28/08/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias Authors = QuoteItemCollection 11 | -------------------------------------------------------------------------------- /Sources/QuoteKit/Model/AuthorsAndTagsSortType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorsAndTagsSortType.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 30/08/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum AuthorsAndTagsSortType: String { 11 | case dateAdded 12 | case dateModified 13 | case name 14 | case quoteCount 15 | } 16 | -------------------------------------------------------------------------------- /Sources/QuoteKit/Model/QuotableListOrder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuotableListOrder.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 30/08/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum QuotableListOrder: String { 11 | case ascending = "asc" 12 | case descending = "desc" 13 | } 14 | -------------------------------------------------------------------------------- /Sources/QuoteKit/Model/Quote.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Quote.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 28/08/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Quote: Identifiable, Equatable, Sendable { 11 | public var id: String 12 | public var tags: [String] 13 | public var content: String 14 | public var author: String 15 | public var authorSlug: String 16 | public var length: Int 17 | public var dateAdded: String 18 | public var dateModified: String 19 | 20 | init( 21 | id: String, tags: [String], content: String, author: String, authorSlug: String, length: Int, 22 | dateAdded: String, dateModified: String 23 | ) { 24 | self.id = id 25 | self.tags = tags 26 | self.content = content 27 | self.author = author 28 | self.authorSlug = authorSlug 29 | self.length = length 30 | self.dateAdded = dateAdded 31 | self.dateModified = dateModified 32 | } 33 | } 34 | 35 | extension Quote: Decodable { 36 | enum CodingKeys: String, CodingKey { 37 | case id = "_id" 38 | case tags, content, author, authorSlug, length, dateAdded, dateModified 39 | } 40 | } 41 | 42 | /// Wrapper for backup API response format 43 | internal struct BackupQuoteResponse: Decodable { 44 | let quote: BackupQuote 45 | } 46 | 47 | /// Quote structure from backup API 48 | internal struct BackupQuote: Decodable { 49 | let id: String 50 | let content: String 51 | let tags: [BackupTag] 52 | let author: BackupAuthor 53 | } 54 | 55 | internal struct BackupTag: Decodable { 56 | let name: String 57 | } 58 | 59 | internal struct BackupAuthor: Decodable { 60 | let name: String 61 | let slug: String 62 | } 63 | 64 | extension Quote { 65 | /// Initialize from backup API response 66 | internal init(from backupQuote: BackupQuote) { 67 | self.id = backupQuote.id 68 | self.content = backupQuote.content 69 | self.author = backupQuote.author.name 70 | self.authorSlug = backupQuote.author.slug 71 | self.tags = backupQuote.tags.map { $0.name } 72 | self.length = backupQuote.content.count 73 | self.dateAdded = "" 74 | self.dateModified = "" 75 | } 76 | } 77 | 78 | extension Quote { 79 | public static let preview = Quote( 80 | id: UUID().uuidString, tags: ["wisdom"], 81 | content: 82 | "Financial freedom is all about doing what you really want and not worry about money at all.", 83 | author: "Rudrank Riyam", authorSlug: "rudrank-riyam", length: 61, 84 | dateAdded: String(describing: Date()), dateModified: String(describing: Date())) 85 | } 86 | -------------------------------------------------------------------------------- /Sources/QuoteKit/Model/QuoteItemCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuoteItemCollection.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 28/08/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// This struct represents a collection of items that can be decoded from JSON data. 11 | /// 12 | /// Use this struct to model a collection of items that can be retrieved from an API response. 13 | /// The struct conforms to the `Decodable` protocol, which means it can be initialized from 14 | /// JSON data. 15 | /// 16 | /// Example usage: 17 | /// 18 | /// ```swift 19 | /// let json = """ 20 | /// { 21 | /// "count": 5, 22 | /// "total_count": 10, 23 | /// "page": 1, 24 | /// "total_pages": 2, 25 | /// "last_item_index": 4, 26 | /// "results": [ 27 | /// {"id": 1, "name": "Item 1"}, 28 | /// {"id": 2, "name": "Item 2"}, 29 | /// {"id": 3, "name": "Item 3"}, 30 | /// {"id": 4, "name": "Item 4"}, 31 | /// {"id": 5, "name": "Item 5"} 32 | /// ] 33 | /// } 34 | /// """.data(using: .utf8)! 35 | /// 36 | /// struct Item: Decodable { 37 | /// let id: Int 38 | /// let name: String 39 | /// } 40 | /// 41 | /// let collection = try JSONDecoder().decode(QuoteItemCollection.self, from: json) 42 | /// print(collection.totalCount) // Prints "10" 43 | /// print(collection.results.count) // Prints "5" 44 | /// ``` 45 | public struct QuoteItemCollection: Decodable, Sendable { 46 | 47 | /// The number of items in the collection. 48 | public var count: Int 49 | 50 | /// The total number of items available. 51 | public var totalCount: Int 52 | 53 | /// The current page number. 54 | public var page: Int 55 | 56 | /// The total number of pages. 57 | public var totalPages: Int 58 | 59 | /// The index of the last item in the collection, if known. 60 | public var lastItemIndex: Int? 61 | 62 | /// The items in the collection. 63 | public var results: [Item] 64 | 65 | /// Initializes a new `QuoteItemCollection` instance with the given properties. 66 | /// 67 | /// - Parameters: 68 | /// - count: The number of items in the collection. 69 | /// - totalCount: The total number of items available. 70 | /// - page: The current page number. 71 | /// - totalPages: The total number of pages. 72 | /// - lastItemIndex: The index of the last item in the collection, if known. 73 | /// - results: The items in the collection. 74 | public init(count: Int, totalCount: Int, page: Int, totalPages: Int, lastItemIndex: Int?, results: [Item]) { 75 | self.count = count 76 | self.totalCount = totalCount 77 | self.page = page 78 | self.totalPages = totalPages 79 | self.lastItemIndex = lastItemIndex 80 | self.results = results 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/QuoteKit/Model/Quotes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Quotes.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 28/08/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A typealias for `QuoteItemCollection` with the generic parameter `Quote`. 11 | /// 12 | /// Use this typealias to create a collection of quotes. 13 | public typealias Quotes = QuoteItemCollection 14 | -------------------------------------------------------------------------------- /Sources/QuoteKit/Model/QuotesSortType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuotesSortType.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 30/08/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An enum that represents the sort type for a collection of quotes. 11 | /// 12 | /// Use this enum to specify how a collection of quotes should be sorted. 13 | /// 14 | /// Example usage: 15 | /// 16 | /// let sortType: QuotesSortType = .author 17 | public enum QuotesSortType: String { 18 | 19 | /// Sort by the date the quote was added. 20 | case dateAdded 21 | 22 | /// Sort by the date the quote was last modified. 23 | case dateModified 24 | 25 | /// Sort by the author of the quote. 26 | case author 27 | 28 | /// Sort by the content of the quote. 29 | case content 30 | } 31 | -------------------------------------------------------------------------------- /Sources/QuoteKit/Model/Tags.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tags.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 28/08/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A typealias for an array of `Tag` instances. 11 | public typealias Tags = [Tag] 12 | 13 | /// A struct that represents a tag. 14 | /// 15 | /// Use this struct to model a tag that can be associated with a quote. 16 | /// The struct conforms to the `Identifiable` and `Hashable` protocols, which 17 | /// means it has an `id` property and can be used in a `Set` or a `Dictionary`. 18 | /// 19 | /// Example usage: 20 | /// 21 | /// ``` 22 | /// let tag = Tag(id: "1", name: "technology", dateAdded: "2022-04-15T12:00:00Z", dateModified: "2022-04-15T12:00:00Z", quoteCount: 5) 23 | /// ``` 24 | public struct Tag: Identifiable, Hashable, Sendable { 25 | 26 | /// The unique identifier for the tag. 27 | public var id: String 28 | 29 | /// The name of the tag. 30 | public var name: String 31 | 32 | /// The date the tag was added, in ISO 8601 format. 33 | public var dateAdded: String 34 | 35 | /// The date the tag was last modified, in ISO 8601 format. 36 | public var dateModified: String 37 | 38 | /// The number of quotes associated with the tag. 39 | public var quoteCount: Int 40 | 41 | /// The capitalized name of the tag, with hyphens replaced by spaces. 42 | public var capitalisedName: String { 43 | name.capitalized.replacingOccurrences(of: "-", with: " ") 44 | } 45 | 46 | /// Initializes a new `Tag` instance with the given properties. 47 | /// 48 | /// - Parameters: 49 | /// - id: The unique identifier for the tag. 50 | /// - name: The name of the tag. 51 | /// - dateAdded: The date the tag was added, in ISO 8601 format. 52 | /// - dateModified: The date the tag was last modified, in ISO 8601 format. 53 | /// - quoteCount: The number of quotes associated with the tag. 54 | public init(id: String, name: String, dateAdded: String, dateModified: String, quoteCount: Int) { 55 | self.id = id 56 | self.name = name 57 | self.dateAdded = dateAdded 58 | self.dateModified = dateModified 59 | self.quoteCount = quoteCount 60 | } 61 | } 62 | 63 | extension Tag: Decodable { 64 | enum CodingKeys: String, CodingKey { 65 | case id = "_id" 66 | case name, dateAdded, dateModified 67 | case quoteCount 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/QuoteKit/Model/URLQueryItemListType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLQueryItemListType.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 30/08/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum URLQueryItemListType { 11 | case all 12 | case either 13 | } 14 | -------------------------------------------------------------------------------- /Sources/QuoteKit/QuoteKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuoteKit.swift 3 | // QuoteKit 4 | // 5 | // Created by Rudrank Riyam on 28/08/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// QuoteKit provides a Swift interface to the Quotable API 11 | /// Uses modern async/await syntax for clean, readable networking code 12 | public struct QuoteKit { 13 | 14 | // MARK: - Session Management 15 | 16 | /// Returns the appropriate URLSession based on environment configuration 17 | /// Uses insecure session if QUOTEKIT_INSECURE_SSL=1 is set (for testing only) 18 | static var session: URLSession { 19 | if ProcessInfo.processInfo.environment["QUOTEKIT_INSECURE_SSL"] == "1" { 20 | return URLSession( 21 | configuration: .default, 22 | delegate: InsecureSessionDelegate(), 23 | delegateQueue: nil 24 | ) 25 | } else { 26 | return URLSession.shared 27 | } 28 | } 29 | 30 | // MARK: - Async/Await Execution 31 | 32 | /// Execute a network request using modern async/await syntax with automatic fallback 33 | /// - Parameter endpoint: The QuotableEndpoint to fetch data from 34 | /// - Returns: Decoded model of the specified type 35 | /// - Throws: Network or decoding errors 36 | static internal func execute( 37 | with endpoint: QuotableEndpoint 38 | ) async throws -> Model { 39 | // Try the configured endpoint first 40 | do { 41 | return try await executeRequest(with: endpoint) 42 | } catch { 43 | // If the primary request fails and we're not already using the backup, 44 | // try the backup API automatically 45 | if endpoint.host.rawValue != "api.quotable.kurokeita.dev" { 46 | let backupEndpoint = QuotableEndpoint( 47 | endpoint.path, 48 | queryItems: endpoint.queryItems, 49 | host: .backup 50 | ) 51 | 52 | do { 53 | return try await executeRequest(with: backupEndpoint) 54 | } catch { 55 | // If backup also fails, throw the original error 56 | throw error 57 | } 58 | } else { 59 | // If we were already using backup and it failed, throw the error 60 | throw error 61 | } 62 | } 63 | } 64 | 65 | /// Execute a single network request 66 | /// - Parameter endpoint: The QuotableEndpoint to fetch data from 67 | /// - Returns: Decoded model of the specified type 68 | /// - Throws: Network or decoding errors 69 | private static func executeRequest( 70 | with endpoint: QuotableEndpoint 71 | ) async throws -> Model { 72 | let (data, response) = try await session.data(from: endpoint.url) 73 | 74 | // Validate HTTP response 75 | guard let httpResponse = response as? HTTPURLResponse, 76 | 200...299 ~= httpResponse.statusCode 77 | else { 78 | throw QuoteFetchError.invalidResponse 79 | } 80 | 81 | // Decode the response based on the API type 82 | let decoder = JSONDecoder() 83 | 84 | // Handle different response formats 85 | if endpoint.host.rawValue == "api.quotable.kurokeita.dev" { 86 | // Backup API has different response format 87 | if Model.self == Quote.self { 88 | let backupResponse = try decoder.decode(BackupQuoteResponse.self, from: data) 89 | let quote = Quote(from: backupResponse.quote) 90 | return quote as! Model 91 | } 92 | } 93 | 94 | // Default decoding for original API format 95 | return try decoder.decode(Model.self, from: data) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Tests/QuoteKitTests/Authors/AuthorsDataTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorsDataTests.swift 3 | // AuthorsDataTests 4 | // 5 | // Created by Rudrank Riyam on 30/08/21. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable import QuoteKit 11 | 12 | final class AuthorsDataTests: XCTestCase { 13 | func testAuthorMatchesParticularID() async throws { 14 | do { 15 | let author = try await QuoteKit.author(id: "XYxYtSeixS-o") 16 | let unwrappedAuthor = try XCTUnwrap(author) 17 | 18 | XCTAssertEqual(unwrappedAuthor.link, "https://en.wikipedia.org/wiki/Aesop") 19 | XCTAssertEqual( 20 | unwrappedAuthor.bio, 21 | "Aesop (c. 620 – 564 BCE) was a Greek fabulist and storyteller credited with a number of fables now collectively known as Aesop's Fables." 22 | ) 23 | XCTAssertEqual(unwrappedAuthor.name, "Aesop") 24 | XCTAssertEqual(unwrappedAuthor.slug, "aesop") 25 | XCTAssertEqual(unwrappedAuthor.description, "Ancient Greek storyteller") 26 | XCTAssertEqual(unwrappedAuthor.quoteCount, 10) 27 | } catch { 28 | XCTFail("Expected author, but failed \(error).") 29 | } 30 | } 31 | 32 | func testAuthorsReturnsManyAuthors() async throws { 33 | do { 34 | let authors = try await QuoteKit.authors() 35 | let unwrappedAuthors = try XCTUnwrap(authors) 36 | 37 | XCTAssertGreaterThan(unwrappedAuthors.count, 1) 38 | } catch { 39 | XCTFail("Expected authors, but failed \(error).") 40 | } 41 | } 42 | 43 | func testAuthorsSearchForParticularQuery() async throws { 44 | do { 45 | let authors = try await QuoteKit.searchAuthors(for: "aesop") 46 | let unwrappedAuthor = try XCTUnwrap(authors.results.first) 47 | 48 | XCTAssertEqual(unwrappedAuthor.link, "https://en.wikipedia.org/wiki/Aesop") 49 | XCTAssertEqual( 50 | unwrappedAuthor.bio, 51 | "Aesop (c. 620 – 564 BCE) was a Greek fabulist and storyteller credited with a number of fables now collectively known as Aesop's Fables." 52 | ) 53 | XCTAssertEqual(unwrappedAuthor.name, "Aesop") 54 | XCTAssertEqual(unwrappedAuthor.slug, "aesop") 55 | XCTAssertEqual(unwrappedAuthor.description, "Ancient Greek storyteller") 56 | XCTAssertEqual(unwrappedAuthor.quoteCount, 10) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/QuoteKitTests/Authors/AuthorsURLTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorsURLTests.swift 3 | // AuthorsURLTests 4 | // 5 | // Created by Rudrank Riyam on 30/08/21. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable import QuoteKit 11 | 12 | final class AuthorsURLTests: XCTestCase { 13 | private let host = QuotableURLHost.production 14 | 15 | func testURLForParticularID() { 16 | let url = QuotableEndpoint(.author("XYxYtSeixS-o")).url 17 | try XCTAssertEqual(url, host.expectedURL(with: "authors/XYxYtSeixS-o")) 18 | } 19 | 20 | func testURLForAuthors() { 21 | let url = QuotableEndpoint(.authors).url 22 | try XCTAssertEqual(url, host.expectedURL(with: "authors")) 23 | } 24 | 25 | func testURLForSpecificAuthor() { 26 | let url = QuotableEndpoint(.authors, queryItems: [.slugs(["aesop"])]).url 27 | try XCTAssertEqual(url, host.expectedURL(with: "authors?slug=aesop")) 28 | } 29 | 30 | func testURLForManyAuthors() { 31 | let url = QuotableEndpoint(.authors, queryItems: [.slugs(["aesop", "theophrastus"])]).url 32 | try XCTAssertEqual(url, host.expectedURL(with: "authors?slug=aesop|theophrastus")) 33 | } 34 | 35 | func testURLWithSortParameter() { 36 | let url = QuotableEndpoint(.authors, queryItems: [.sortBy(.dateModified)]).url 37 | try XCTAssertEqual(url, host.expectedURL(with: "authors?sortBy=dateModified")) 38 | } 39 | 40 | func testURLWithOrderParameter() { 41 | let url = QuotableEndpoint(.authors, queryItems: [.order(.descending)]).url 42 | try XCTAssertEqual(url, host.expectedURL(with: "authors?order=desc")) 43 | } 44 | 45 | func testURLWithSortAndOrderParameter() { 46 | let url = QuotableEndpoint(.authors, queryItems: [.sortBy(.dateAdded), .order(.ascending)]).url 47 | try XCTAssertEqual(url, host.expectedURL(with: "authors?sortBy=dateAdded&order=asc")) 48 | } 49 | 50 | func testURLWithLimitParameter() { 51 | let url = QuotableEndpoint(.authors, queryItems: [.limit(50)]).url 52 | try XCTAssertEqual(url, host.expectedURL(with: "authors?limit=50")) 53 | } 54 | 55 | func testURLWithPagesParameter() { 56 | let url = QuotableEndpoint(.authors, queryItems: [.page(2)]).url 57 | try XCTAssertEqual(url, host.expectedURL(with: "authors?page=2")) 58 | } 59 | 60 | func testURLWithLimitAndPagesParameter() { 61 | let url = QuotableEndpoint(.authors, queryItems: [.limit(50), .page(2)]).url 62 | try XCTAssertEqual(url, host.expectedURL(with: "authors?limit=50&page=2")) 63 | } 64 | 65 | func testURLforSearchAuthors() { 66 | let url = QuotableEndpoint(.searchAuthors, queryItems: [.search("kalam")]).url 67 | try XCTAssertEqual(url, host.expectedURL(with: "search/authors?query=kalam")) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/QuoteKitTests/QuotableURLHost.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuotableURLHost.swift 3 | // QuotableURLHost 4 | // 5 | // Created by Rudrank Riyam on 30/08/21. 6 | // 7 | 8 | @testable import QuoteKit 9 | import XCTest 10 | 11 | extension QuotableURLHost { 12 | func expectedURL(with path: String) throws -> URL { 13 | let encodedPath = path.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" 14 | 15 | let url = URL(string: "https://" + rawValue + "/" + encodedPath) 16 | return try XCTUnwrap(url) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/QuoteKitTests/Quotes/QuotesDataTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuotesDataTests.swift 3 | // QuotesDataTests 4 | // 5 | // Created by Rudrank Riyam on 30/08/21. 6 | // 7 | 8 | @testable import QuoteKit 9 | import XCTest 10 | 11 | final class QuotesDataTests: XCTestCase { 12 | func testQuoteForParticularID() async throws { 13 | do { 14 | let quote = try await QuoteKit.quote(id: "2xpHvSOQMD") 15 | let unwrappedQuote = try XCTUnwrap(quote) 16 | 17 | XCTAssertEqual(unwrappedQuote.tags, ["Famous Quotes", "Inspirational"]) 18 | XCTAssertEqual(unwrappedQuote.id, "2xpHvSOQMD") 19 | XCTAssertEqual(unwrappedQuote.author, "Helmut Schmidt") 20 | XCTAssertEqual(unwrappedQuote.content, "The biggest room in the world is room for improvement.") 21 | XCTAssertEqual(unwrappedQuote.authorSlug, "helmut-schmidt") 22 | XCTAssertEqual(unwrappedQuote.length, 54) 23 | XCTAssertEqual(unwrappedQuote.dateAdded, "2021-06-18") 24 | XCTAssertEqual(unwrappedQuote.dateModified, "2023-04-14") 25 | } catch { 26 | XCTFail("Expected quote, but failed \(error).") 27 | } 28 | } 29 | 30 | func testQuotesReturnsManyQuotes() async throws { 31 | do { 32 | let quotes = try await QuoteKit.quotes() 33 | let unwrappedQuotes = try XCTUnwrap(quotes) 34 | 35 | XCTAssertGreaterThan(unwrappedQuotes.count, 1) 36 | } catch { 37 | XCTFail("Expected quotes, but failed \(error).") 38 | } 39 | } 40 | 41 | func testQuotesSearchForParticularQuery() async throws { 42 | do { 43 | let quotes = try await QuoteKit.searchQuotes(for: "biggest room") 44 | let unwrappedQuote = try XCTUnwrap(quotes.results.first) 45 | 46 | XCTAssertEqual(unwrappedQuote.tags, ["Famous Quotes", "Inspirational"]) 47 | XCTAssertEqual(unwrappedQuote.id, "2xpHvSOQMD") 48 | XCTAssertEqual(unwrappedQuote.author, "Helmut Schmidt") 49 | XCTAssertEqual(unwrappedQuote.content, "The biggest room in the world is room for improvement.") 50 | XCTAssertEqual(unwrappedQuote.authorSlug, "helmut-schmidt") 51 | XCTAssertEqual(unwrappedQuote.length, 54) 52 | XCTAssertEqual(unwrappedQuote.dateAdded, "2021-06-18") 53 | XCTAssertEqual(unwrappedQuote.dateModified, "2023-04-14") 54 | } catch { 55 | XCTFail("Expected quote, but failed \(error).") 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/QuoteKitTests/Quotes/QuotesURLTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuotesURLTests.swift 3 | // QuotesURLTests 4 | // 5 | // Created by Rudrank Riyam on 30/08/21. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable import QuoteKit 11 | 12 | final class QuotesURLTests: XCTestCase { 13 | private let host = QuotableURLHost.production 14 | 15 | func testURLForParticularID() { 16 | let url = QuotableEndpoint(.quote("_XB2MKPzW7dA")).url 17 | try XCTAssertEqual(url, host.expectedURL(with: "quotes/_XB2MKPzW7dA")) 18 | } 19 | 20 | func testURLForQuotes() { 21 | let url = QuotableEndpoint(.quotes).url 22 | try XCTAssertEqual(url, host.expectedURL(with: "quotes")) 23 | } 24 | 25 | func testURLWithEitherOfTheProvidedTagsParameter() { 26 | let url = QuotableEndpoint(.quotes, queryItems: [.tags(["love", "happiness"], .either)]).url 27 | try XCTAssertEqual(url, host.expectedURL(with: "quotes?tags=love|happiness")) 28 | } 29 | 30 | func testURLWithAllOfTheProvidedTagsParameter() { 31 | let url = QuotableEndpoint(.quotes, queryItems: [.tags(["technology", "famous-quotes"], .all)]) 32 | .url 33 | try XCTAssertEqual(url, host.expectedURL(with: "quotes?tags=technology,famous-quotes")) 34 | } 35 | 36 | func testURLForSpecificAuthor() { 37 | let url = QuotableEndpoint(.quotes, queryItems: [.authors(["albert-einstein"])]).url 38 | try XCTAssertEqual(url, host.expectedURL(with: "quotes?author=albert-einstein")) 39 | } 40 | 41 | func testURLForManyAuthors() { 42 | let url = QuotableEndpoint( 43 | .quotes, queryItems: [.authors(["albert-einstein", "ed-cunningham"])] 44 | ).url 45 | try XCTAssertEqual(url, host.expectedURL(with: "quotes?author=albert-einstein|ed-cunningham")) 46 | } 47 | 48 | func testURLWithSortParameter() { 49 | let url = QuotableEndpoint(.quotes, queryItems: [.sortQuotesBy(.dateAdded)]).url 50 | try XCTAssertEqual(url, host.expectedURL(with: "quotes?sortBy=dateAdded")) 51 | } 52 | 53 | func testURLWithOrderParameter() { 54 | let url = QuotableEndpoint(.quotes, queryItems: [.order(.descending)]).url 55 | try XCTAssertEqual(url, host.expectedURL(with: "quotes?order=desc")) 56 | } 57 | 58 | func testURLWithSortAndOrderParameter() { 59 | let url = QuotableEndpoint(.quotes, queryItems: [.sortQuotesBy(.content), .order(.ascending)]) 60 | .url 61 | try XCTAssertEqual(url, host.expectedURL(with: "quotes?sortBy=content&order=asc")) 62 | } 63 | 64 | func testURLWithLimitParameter() { 65 | let url = QuotableEndpoint(.quotes, queryItems: [.limit(50)]).url 66 | try XCTAssertEqual(url, host.expectedURL(with: "quotes?limit=50")) 67 | } 68 | 69 | func testURLWithPagesParameter() { 70 | let url = QuotableEndpoint(.quotes, queryItems: [.page(2)]).url 71 | try XCTAssertEqual(url, host.expectedURL(with: "quotes?page=2")) 72 | } 73 | 74 | func testURLWithLimitAndPagesParameter() { 75 | let url = QuotableEndpoint(.quotes, queryItems: [.limit(50), .page(2)]).url 76 | try XCTAssertEqual(url, host.expectedURL(with: "quotes?limit=50&page=2")) 77 | } 78 | func testURLforSearchQuotes() { 79 | let url = QuotableEndpoint(.searchQuotes, queryItems: [.search("love")]).url 80 | try XCTAssertEqual(url, host.expectedURL(with: "search/quotes?query=love")) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Tests/QuoteKitTests/Quotes/RandomQuoteURLTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RandomQuoteURLTests.swift 3 | // RandomQuoteURLTests 4 | // 5 | // Created by Rudrank Riyam on 30/08/21. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable import QuoteKit 11 | 12 | final class RandomQuoteURLTests: XCTestCase { 13 | private let host = QuotableURLHost.production 14 | 15 | func testURLForRandomQuote() { 16 | let url = QuotableEndpoint(.randomQuote).url 17 | try XCTAssertEqual(url, host.expectedURL(with: "random")) 18 | } 19 | 20 | func testURLForMinimumLengthQuote() { 21 | let url = QuotableEndpoint(.randomQuote, queryItems: [.minLength(10)]).url 22 | try XCTAssertEqual(url, host.expectedURL(with: "random?minLength=10")) 23 | } 24 | 25 | func testURLForMaximumLengthQuote() { 26 | let url = QuotableEndpoint(.randomQuote, queryItems: [.maxLength(100)]).url 27 | try XCTAssertEqual(url, host.expectedURL(with: "random?maxLength=100")) 28 | } 29 | 30 | func testURLForMinimumAndMaximumLengthQuote() { 31 | let url = QuotableEndpoint(.randomQuote, queryItems: [.minLength(10), .maxLength(100)]).url 32 | try XCTAssertEqual(url, host.expectedURL(with: "random?minLength=10&maxLength=100")) 33 | } 34 | 35 | func testURLWithEitherOfTheProvidedTagsParameter() { 36 | let url = QuotableEndpoint(.randomQuote, queryItems: [.tags(["love", "happiness"], .either)]) 37 | .url 38 | try XCTAssertEqual(url, host.expectedURL(with: "random?tags=love|happiness")) 39 | } 40 | 41 | func testURLWithAllOfTheProvidedTagsParameter() { 42 | let url = QuotableEndpoint( 43 | .randomQuote, queryItems: [.tags(["technology", "famous-quotes"], .all)] 44 | ).url 45 | try XCTAssertEqual(url, host.expectedURL(with: "random?tags=technology,famous-quotes")) 46 | } 47 | 48 | func testURLForSpecificAuthor() { 49 | let url = QuotableEndpoint(.randomQuote, queryItems: [.authors(["albert-einstein"])]).url 50 | try XCTAssertEqual(url, host.expectedURL(with: "random?author=albert-einstein")) 51 | } 52 | 53 | func testURLForManyAuthors() { 54 | let url = QuotableEndpoint( 55 | .randomQuote, queryItems: [.authors(["albert-einstein", "ed-cunningham"])] 56 | ).url 57 | try XCTAssertEqual(url, host.expectedURL(with: "random?author=albert-einstein|ed-cunningham")) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/QuoteKitTests/Tags/QuoteKitTagsDataTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuoteKitTagsDataTests.swift 3 | // QuoteKitTagsDataTests 4 | // 5 | // Created by Rudrank Riyam on 30/08/21. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable import QuoteKit 11 | 12 | final class QuoteKitTagsDataTests: XCTestCase { 13 | func testTagsReturnsManyTags() async throws { 14 | do { 15 | let tags = try await QuoteKit.tags() 16 | let unwrappedTags = try XCTUnwrap(tags) 17 | XCTAssertGreaterThan(unwrappedTags.count, 1) 18 | } catch { 19 | XCTFail("Expected tags, but failed \(error).") 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/QuoteKitTests/Tags/QuoteKitTagsURLTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuoteKitTagsURLTests.swift 3 | // QuoteKitTagsURLTests 4 | // 5 | // Created by Rudrank Riyam on 30/08/21. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable import QuoteKit 11 | 12 | final class QuoteKitTagsURLTests: XCTestCase { 13 | private let host = QuotableURLHost.production 14 | 15 | func testURLWithSortParameter() { 16 | let url = QuotableEndpoint(.tags, queryItems: [.sortBy(.dateModified)]).url 17 | try XCTAssertEqual(url, host.expectedURL(with: "tags?sortBy=dateModified")) 18 | } 19 | 20 | func testURLWithOrderParameter() { 21 | let url = QuotableEndpoint(.tags, queryItems: [.order(.descending)]).url 22 | try XCTAssertEqual(url, host.expectedURL(with: "tags?order=desc")) 23 | } 24 | 25 | func testURLWithSortAndOrderParameter() { 26 | let url = QuotableEndpoint(.tags, queryItems: [.sortBy(.dateAdded), .order(.ascending)]).url 27 | try XCTAssertEqual(url, host.expectedURL(with: "tags?sortBy=dateAdded&order=asc")) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /codemagic.yaml: -------------------------------------------------------------------------------- 1 | workflows: 2 | quotekit-workflow: 3 | name: QuoteKit Building and Testing Workflow 4 | instance_type: mac_mini_m1 5 | environment: 6 | xcode: latest 7 | vars: 8 | XCODE_SCHEME: QuoteKit 9 | triggering: 10 | events: 11 | - push 12 | scripts: 13 | - name: Build Framework 14 | script: | 15 | #!/bin/zsh 16 | 17 | declare -a DESTINATIONS=("platform=iOS Simulator,name=iPhone 14" "platform=watchOS Simulator,name=Apple Watch Series 8 (45mm)" "platform=tvOS Simulator,name=Apple TV 4K (3rd generation)" "platform=macOS") 18 | for DESTINATION in "${DESTINATIONS[@]}" 19 | do 20 | xcodebuild clean build \ 21 | -scheme "$XCODE_SCHEME" \ 22 | -destination "$DESTINATION" \ 23 | -skipPackagePluginValidation 24 | done 25 | - name: Test Framework 26 | script: | 27 | #!/bin/zsh 28 | 29 | declare -a DESTINATIONS=("platform=iOS Simulator,name=iPhone 14" "platform=watchOS Simulator,name=Apple Watch Series 8 (45mm)" "platform=tvOS Simulator,name=Apple TV 4K (3rd generation)" "platform=macOS") 30 | for DESTINATION in "${DESTINATIONS[@]}" 31 | do 32 | set -o pipefail 33 | xcodebuild clean test \ 34 | -scheme "$XCODE_SCHEME" \ 35 | -destination "$DESTINATION" \ 36 | -skipPackagePluginValidation | xcpretty --report junit 37 | done 38 | test_report: build/reports/junit.xml 39 | -------------------------------------------------------------------------------- /releases/v2.0.0-release-notes.md: -------------------------------------------------------------------------------- 1 | # QuoteKit 2.0.0 Release Notes 2 | 3 | ## 🚀 Major Features 4 | 5 | - **Swift 6 Compatibility**: Full support for Swift 6 with strict concurrency checking 6 | - **Modern Async/Await**: Complete migration to async/await networking patterns 7 | - **Enhanced Platform Support**: Updated minimum requirements to iOS 15.0+, macOS 12.0+, watchOS 8.0+, tvOS 15.0+ 8 | 9 | ## ✨ New Features 10 | 11 | - Added `Sendable` conformance to all data models (`Quote`, `Author`, `Tag`, `QuoteItemCollection`) 12 | - Introduced `InsecureSessionDelegate` for testing environments with SSL bypass 13 | - Comprehensive SwiftLint integration for code quality 14 | 15 | ## 🔧 Improvements 16 | 17 | - Refactored test cases for improved readability and consistency 18 | - Fixed concurrency safety issues with static properties 19 | - Enhanced hash implementation for `Author` model 20 | - Improved code formatting and style compliance 21 | 22 | ## 📦 Dependencies 23 | 24 | - Swift Tools Version: 6.0 25 | - SwiftLint Plugin: 0.2.2+ 26 | - Swift DocC Plugin: 1.0.0+ 27 | 28 | ## 🧪 Testing 29 | 30 | - All URL generation tests passing 31 | - SwiftLint compliance with zero violations 32 | - Comprehensive async/await test coverage 33 | 34 | ## 💥 Breaking Changes 35 | 36 | - Minimum platform requirements increased 37 | - Models now require `Sendable` conformance 38 | - Static `preview` property changed from `var` to `let` 39 | 40 | This is a major release that modernizes QuoteKit for the latest Swift ecosystem while maintaining backward compatibility for the public API. --------------------------------------------------------------------------------