From de7a9716799beba2807bed6c69482172cdc6179c Mon Sep 17 00:00:00 2001 From: Vincent Tourraine Date: Thu, 7 Dec 2023 23:02:54 +0100 Subject: [PATCH] Get license from GitHub API --- CHANGELOG.md | 6 ++ .../AcknowList/AcknowListViewController.swift | 52 ++++++++++--- Sources/AcknowList/GitHubAPI.swift | 76 +++++++++++++++++++ Tests/AcknowListTests/GitHubAPITests.swift | 31 ++++++++ 4 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 Sources/AcknowList/GitHubAPI.swift create mode 100644 Tests/AcknowListTests/GitHubAPITests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 23847a0..ae4b174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 3.1 (work in progress) + +- Add `GitHubAPI` to get licenses from GitHub API +- Update `AcknowListViewController` to get missing licenses from GitHub API, with new `canFetchLicenseFromGitHub` property to disable this behavior + + ## 3.0.1 (24 November 2022) - Update `AcknowListSwiftUIView` to fix navigation to repository URL diff --git a/Sources/AcknowList/AcknowListViewController.swift b/Sources/AcknowList/AcknowListViewController.swift index 6a354d7..7b84dd1 100644 --- a/Sources/AcknowList/AcknowListViewController.swift +++ b/Sources/AcknowList/AcknowListViewController.swift @@ -37,6 +37,9 @@ open class AcknowListViewController: UITableViewController { /// The represented array of `Acknow`. open var acknowledgements: [Acknow] = [] + /// Indicates if the view controller should try to fetch missing licenses from the GitHub API. + open var canFetchLicenseFromGitHub = true + /** Header text to be displayed above the list of the acknowledgements. It needs to get set before `viewDidLoad` gets called. @@ -385,18 +388,30 @@ open class AcknowListViewController: UITableViewController { - indexPath: An index path locating the new selected row in `tableView`. */ open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let acknowledgement = acknowledgements[(indexPath as NSIndexPath).row] as Acknow?, + if let acknowledgement = acknowledgements[indexPath.row] as Acknow?, let navigationController = navigationController { if acknowledgement.text != nil { let viewController = AcknowViewController(acknowledgement: acknowledgement) navigationController.pushViewController(viewController, animated: true) } - else if canOpenRepository(for: acknowledgement), - let repository = acknowledgement.repository { -#if !os(tvOS) - let safariViewController = SFSafariViewController(url: repository) - present(safariViewController, animated: true) -#endif + else if canFetchLicenseFromGitHub, + let repository = acknowledgement.repository, + GitHubAPI.isGitHubRepository(repository) { + GitHubAPI.getLicense(for: repository) { [weak self] result in + switch result { + case .success(let text): + let updatedAcknowledgement = Acknow(title: acknowledgement.title, text: text, license: acknowledgement.license, repository: acknowledgement.repository) + self?.acknowledgements[indexPath.row] = updatedAcknowledgement + let viewController = AcknowViewController(acknowledgement: updatedAcknowledgement) + navigationController.pushViewController(viewController, animated: true) + + case .failure: + self?.openRepository(repository) + } + } + } + else if let repository = acknowledgement.repository { + openRepository(repository) } } } @@ -415,16 +430,33 @@ open class AcknowListViewController: UITableViewController { // MARK: - Navigation private func canOpen(_ acknowledgement: Acknow) -> Bool { - return acknowledgement.text != nil || canOpenRepository(for: acknowledgement) + if acknowledgement.text != nil { + return true + } + else if let repository = acknowledgement.repository { + return canOpenRepository(repository) + } + else { + return false + } } - private func canOpenRepository(for acknowledgement: Acknow) -> Bool { - guard let scheme = acknowledgement.repository?.scheme else { + private func canOpenRepository(_ repository: URL) -> Bool { + guard let scheme = repository.scheme else { return false } return scheme == "http" || scheme == "https" } + + private func openRepository(_ repository: URL) { +#if !os(tvOS) + if canOpenRepository(repository) { + let safariViewController = SFSafariViewController(url: repository) + present(safariViewController, animated: true) + } +#endif + } } #endif diff --git a/Sources/AcknowList/GitHubAPI.swift b/Sources/AcknowList/GitHubAPI.swift new file mode 100644 index 0000000..eba9dc2 --- /dev/null +++ b/Sources/AcknowList/GitHubAPI.swift @@ -0,0 +1,76 @@ +// +// GitHubAPI.swift +// +// Copyright (c) 2015-2023 Vincent Tourraine (https://www.vtourraine.net) +// +// 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 object that interacts with the GitHub API. +public class GitHubAPI { + + /** + Gets the repository license. + - Parameters: + - repository: The GitHub URL for the repository. For example: `https://github.com/vtourraine/AcknowList.git` + - completionHandler: The completion handler to call when the load request is complete. This handler is executed on the main queue. It takes a `Result` parameter, with either the body of the license, or an error object that indicates why the request failed. + */ + @discardableResult public static func getLicense(for repository: URL, completionHandler: @escaping (Result) -> Void) -> URLSessionDataTask { + // GitHub API documentation + // https://docs.github.com/en/rest/licenses/licenses#get-the-license-for-a-repository + + let request = getLicenseRequest(for: repository) + let task = URLSession.shared.dataTask(with: request) { (data, response, error) in + DispatchQueue.main.async { + if (response as? HTTPURLResponse)?.statusCode == 200, + let data, + let text = String(data: data, encoding: .utf8) { + completionHandler(.success(text)) + } + else { + completionHandler(.failure(error ?? URLError(URLError.Code.unknown))) + } + } + } + + task.resume() + return task + } + + /** + Returns a Boolean value indicating whether a URL is a valid GitHub repository URL. + - Parameter repository: The GitHub URL for the repository. For example: `https://github.com/vtourraine/AcknowList.git` + */ + public static func isGitHubRepository(_ repository: URL) -> Bool { + return repository.absoluteString.hasPrefix("https://github.com/") + } + + internal static func getLicenseRequest(for repository: URL) -> URLRequest { + let path = pathWithoutExtension(for: repository) + let url = "https://api.github.com/repos\(path)/license" + var request = URLRequest(url: URL(string: url)!) + request.allHTTPHeaderFields = ["Accept": "application/vnd.github.raw"] + return request + } + + internal static func pathWithoutExtension(for repository: URL) -> String { + return repository.path.replacingOccurrences(of: ".git", with: "") + } +} diff --git a/Tests/AcknowListTests/GitHubAPITests.swift b/Tests/AcknowListTests/GitHubAPITests.swift new file mode 100644 index 0000000..3822013 --- /dev/null +++ b/Tests/AcknowListTests/GitHubAPITests.swift @@ -0,0 +1,31 @@ +// +// GitHubAPITests.swift +// AcknowExampleTests +// +// Created by Vincent Tourraine on 07/12/2023. +// Copyright © 2015-2022 Vincent Tourraine. All rights reserved. +// + +import XCTest + +@testable import AcknowList + +class GitHubAPITests: XCTestCase { + + func testRecognizeGitHubRepository() { + let repoURL = URL(string: "https://github.com/vtourraine/AcknowList.git")! + XCTAssertTrue(GitHubAPI.isGitHubRepository(repoURL)) + + let otherURL = URL(string: "https://www.website.com")! + XCTAssertFalse(GitHubAPI.isGitHubRepository(otherURL)) + } + + func testGetLicenseRequest() { + let repoURL = URL(string: "https://github.com/vtourraine/AcknowList.git")! + let request = GitHubAPI.getLicenseRequest(for: repoURL) + + XCTAssertEqual(request.url?.absoluteString, "https://api.github.com/repos/vtourraine/AcknowList/license") + XCTAssertEqual(request.allHTTPHeaderFields, ["Accept": "application/vnd.github.raw"]) + XCTAssertEqual(request.httpMethod, "GET") + } +}