Skip to content

Commit

Permalink
v1.3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
ezefranca committed Aug 11, 2024
1 parent da4e042 commit a034d04
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 68 deletions.
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,12 @@ let article = try await fetcher.fetchArticle(articleLink: articleLink)
print(article)
```

### `fetchScientistDetails`
### `fetchAuthorDetails`

Fetches the scientist's details such as name, affiliation, and picture URL from Google Scholar.
Fetches the Author's details such as name, affiliation, and picture URL from Google Scholar.

```swift
public func fetchScientistDetails(scholarID: GoogleScholarID) async throws -> Scientist
public func fetchAuthorDetails(scholarID: GoogleScholarID) async throws -> Author
```

#### Parameters
Expand All @@ -115,7 +115,7 @@ public func fetchScientistDetails(scholarID: GoogleScholarID) async throws -> Sc

#### Returns

- A `Scientist` object containing the scientist's details.
- A `Author` object containing the Author's details.

#### Throws

Expand All @@ -126,8 +126,8 @@ public func fetchScientistDetails(scholarID: GoogleScholarID) async throws -> Sc
```swift
let fetcher = GoogleScholarFetcher()

let scientistDetails = try await fetcher.fetchScientistDetails(scholarID: GoogleScholarID("6nOPl94AAAAJ"))
print(scientistDetails)
let AuthorDetails = try await fetcher.fetchAuthorDetails(scholarID: GoogleScholarID("6nOPl94AAAAJ"))
print(AuthorDetails)
```

### Complete Example
Expand Down Expand Up @@ -157,9 +157,9 @@ if let firstPublication = publications.first {
print("Article Details:", article)
}

// Fetch scientist details
let scientistDetails = try await fetcher.fetchScientistDetails(scholarID: authorID)
print("Scientist Details:", scientistDetails)
// Fetch Author details
let AuthorDetails = try await fetcher.fetchAuthorDetails(scholarID: authorID)
print("Author Details:", AuthorDetails)
```

## Contributing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import SwiftSoup
/// A class responsible for fetching data from Google Scholar.
public class GoogleScholarFetcher {
private let session: URLSession
private var publicationCache: [GoogleScholarID: [Publication]] = [:]

// MARK: Public Methods

Expand All @@ -26,7 +27,7 @@ public class GoogleScholarFetcher {
/// - Example:
/// ```swift
/// let fetcher = GoogleScholarFetcher()
/// let publications = try await fetcher.fetchAllPublications(authorID: GoogleScholarID("6nOPl94AAAAJ"), fetchQuantity: .specific(10))
/// let publications = try await fetcher.fetchAllPublications(authorID: GoogleScholarID("RefX_60AAAAJ"), fetchQuantity: .specific(10))
/// print(publications)
/// ```
public func fetchAllPublications(
Expand Down Expand Up @@ -178,19 +179,19 @@ public class GoogleScholarFetcher {
return publications
}

/// Fetches the scientist's details such as name, affiliation, and picture URL from Google Scholar.
/// Fetches the author's details such as name, affiliation, and picture URL from Google Scholar.
///
/// - Parameter scholarID: The Google Scholar author ID.
/// - Returns: A `Scientist` object containing the scientist's details.
/// - Returns: A `Author` object containing the author's details.
/// - Throws: An error if fetching or parsing fails.
///
/// - Example:
/// ```swift
/// let fetcher = GoogleScholarFetcher()
/// let scientistDetails = try await fetcher.fetchScientistDetails(scholarID: GoogleScholarID("6nOPl94AAAAJ"))
/// print(scientistDetails)
/// let authorDetails = try await fetcher.fetchAuthorDetails(scholarID: GoogleScholarID("RefX_60AAAAJ"))
/// print(authorDetails)
/// ```
public func fetchScientistDetails(scholarID: GoogleScholarID) async throws -> Scientist {
public func fetchAuthorDetails(scholarID: GoogleScholarID) async throws -> Author {
guard var urlComponents = URLComponents(string: "https://scholar.google.com/citations") else {
throw NSError(domain: "Invalid URL", code: 0, userInfo: nil)
}
Expand All @@ -216,7 +217,36 @@ public class GoogleScholarFetcher {
throw NSError(domain: "Invalid Data", code: 0, userInfo: nil)
}

return try parseScientistDetails(from: html, id: scholarID)
return try parseAuthorDetails(from: html, id: scholarID)
}

/// Fetches the total number of citations and publications for a given author from Google Scholar.
///
/// - Parameters:
/// - authorID: The Google Scholar author ID.
/// - fetchQuantity: The quantity of publications to fetch. Can be `.all` or `.specific(Int)`. Defaults to `.all`.
/// - Returns: An `AuthorMetrics` struct containing the total number of citations and total number of publications.
/// - Throws: An error if fetching or parsing fails.
///
/// - Example:
/// ```swift
/// let fetcher = GoogleScholarFetcher()
/// let metrics = try await fetcher.getAuthorMetrics(authorID: GoogleScholarID("RefX_60AAAAJ"))
/// print("Total Citations: \(metrics.citations), Total Publications: \(metrics.publications)")
/// ```
public func getAuthorMetrics(
authorID: GoogleScholarID,
fetchQuantity: FetchQuantity = .all
) async throws -> AuthorMetrics {
let publications = try await fetchAllPublications(authorID: authorID, fetchQuantity: fetchQuantity)

let totalCitations = publications.reduce(into: 0) { (sum, publication) in
sum += Int(publication.citations.onlyNumbers) ?? 0
}

let totalPublications = publications.count

return AuthorMetrics(citations: totalCitations, publications: totalPublications)
}

// MARK: Private Methods
Expand Down Expand Up @@ -297,21 +327,27 @@ public class GoogleScholarFetcher {
return ""
}

/// Parses the scientist's details from the HTML string.
/// Parses the author's details from the HTML string.
///
/// - Parameters:
/// - html: The HTML string to parse.
/// - id: The Google Scholar author ID.
/// - Returns: A `Scientist` object containing the scientist's details.
/// - Returns: A `Author` object containing the author's details.
/// - Throws: An error if parsing fails.
private func parseScientistDetails(from html: String, id: GoogleScholarID) throws -> Scientist {
private func parseAuthorDetails(from html: String, id: GoogleScholarID) throws -> Author {
let doc: Document = try SwiftSoup.parse(html)

let name = try doc.select("#gsc_prf_in").text()
let affiliation = try doc.select(".gsc_prf_il").first()?.text() ?? ""
let pictureURL = try doc.select("#gsc_prf_pua img").attr("src")

return Scientist(id: id, name: name, affiliation: affiliation, pictureURL: pictureURL)
return Author(id: id, name: name, affiliation: affiliation, pictureURL: pictureURL)
}
}

extension String {
/// A computed property that returns only the numeric characters in the string.
var onlyNumbers: String {
return self.filter { $0.isNumber }
}
}
31 changes: 31 additions & 0 deletions Sources/GoogleScholarSwift/Models/Author.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation

/// Model for a author's details.
public struct Author: Codable, Hashable, Identifiable, Equatable, CustomStringConvertible {
/// Unique identifier for the author.
public let id: GoogleScholarID
/// The name of the author.
public let name: String
/// The affiliation of the author.
public let affiliation: String
/// URL of the author's picture.
public let pictureURL: String

/// Initializes a new `Author` instance.
///
/// - Parameters:
/// - id: The Google Scholar author ID.
/// - name: The name of the author.
/// - affiliation: The affiliation of the author.
/// - pictureURL: The URL of the author's picture.
public init(id: GoogleScholarID, name: String, affiliation: String, pictureURL: String) {
self.id = id
self.name = name
self.affiliation = affiliation
self.pictureURL = pictureURL
}

public var description: String {
return "Author(id: \(id), name: \(name), affiliation: \(affiliation), pictureURL: \(pictureURL))"
}
}
24 changes: 24 additions & 0 deletions Sources/GoogleScholarSwift/Models/AuthorMetrics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Foundation

/// A struct representing the total citations and publications for a given author.
public struct AuthorMetrics: Codable, Hashable, Equatable, CustomStringConvertible {

/// The total number of citations across all fetched publications.
public let citations: Int

/// The total number of publications fetched.
public let publications: Int

/// Initializes a new `AuthorMetrics` instance.
/// - Parameters:
/// - citations: The total number of citations.
/// - publications: The total number of publications.
public init(citations: Int, publications: Int) {
self.citations = citations
self.publications = publications
}

public var description: String {
return "AuthorMetrics(citations: \(citations), publications: \(publications)"
}
}
31 changes: 0 additions & 31 deletions Sources/GoogleScholarSwift/Models/Scientist.swift

This file was deleted.

50 changes: 33 additions & 17 deletions Tests/GoogleScholarFetcherTests/GoogleScholarSwiftTests.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import XCTest
@testable import GoogleScholarSwift

// Take care because this is not unity tests, it's more End to end tests (needs internet)
// Take care because these are not unit tests, but more end-to-end tests (require internet)

final class GoogleScholarFetcherTests: XCTestCase {

Expand All @@ -26,7 +26,7 @@ final class GoogleScholarFetcherTests: XCTestCase {
do {
let publications = try await fetcher.fetchAllPublications(authorID: authorID, fetchQuantity: fetchQuantity, sortBy: .pubdate)
XCTAssertEqual(publications.count, 1, "Number of publications should match the limit")
XCTAssertTrue(Int(publications[0].year) ?? 0 >= 2023)
XCTAssertTrue(Int(publications[0].year) ?? 0 >= 2023, "Publication year should be 2023 or later")
} catch {
XCTFail("Error fetching publications: \(error)")
}
Expand All @@ -40,7 +40,7 @@ final class GoogleScholarFetcherTests: XCTestCase {
do {
let publications = try await fetcher.fetchAllPublications(authorID: authorID, fetchQuantity: fetchQuantity, sortBy: .cited)
XCTAssertEqual(publications.count, 1, "Number of publications should match the limit")
XCTAssertTrue(Int(publications[0].citations) ?? 0 > 2400)
XCTAssertTrue(Int(publications[0].citations.onlyNumbers) ?? 0 > 2400, "Citations should be greater than 2400")
} catch {
XCTFail("Error fetching publications: \(error)")
}
Expand Down Expand Up @@ -73,30 +73,46 @@ final class GoogleScholarFetcherTests: XCTestCase {
do {
let article = try await fetcher.fetchArticle(articleLink: articleLink)
XCTAssertNotNil(article, "Article should not be nil")
XCTAssertNotNil(article.title)
XCTAssertNotNil(article.authors)
XCTAssertNotNil(article.publicationDate)
XCTAssertNotNil(article.publication)
XCTAssertNotNil(article.description)
XCTAssertNotNil(article.totalCitations)
XCTAssertNotNil(article.title, "Article title should not be nil")
XCTAssertNotNil(article.authors, "Article authors should not be nil")
XCTAssertNotNil(article.publicationDate, "Article publication date should not be nil")
XCTAssertNotNil(article.publication, "Article publication should not be nil")
XCTAssertNotNil(article.description, "Article description should not be nil")
XCTAssertNotNil(article.totalCitations.onlyNumbers, "Article total citations should not be nil")
} catch {
XCTFail("Error fetching article details: \(error)")
}
}

func test_FetchScientistDetails() async throws {
func test_FetchAuthorDetails() async throws {
let fetcher = GoogleScholarFetcher()
let scholarID = GoogleScholarID("RefX_60AAAAJ")

do {
let scientist = try await fetcher.fetchScientistDetails(scholarID: scholarID)
XCTAssertNotNil(scientist, "Scientist should not be nil")
XCTAssertEqual(scientist.id, scholarID)
XCTAssertNotNil(scientist.name)
XCTAssertNotNil(scientist.affiliation)
XCTAssertNotNil(scientist.pictureURL)
let author = try await fetcher.fetchAuthorDetails(scholarID: scholarID)
XCTAssertNotNil(author, "Author should not be nil")
XCTAssertEqual(author.id, scholarID)
XCTAssertNotNil(author.name, "Author name should not be nil")
XCTAssertNotNil(author.affiliation, "Author affiliation should not be nil")
XCTAssertNotNil(author.pictureURL, "Author picture URL should not be nil")
} catch {
XCTFail("Error fetching scientist details: \(error)")
XCTFail("Error fetching author details: \(error)")
}
}

func test_AuthorMetrics() async throws {
let fetcher = GoogleScholarFetcher()
let authorID = GoogleScholarID("RefX_60AAAAJ")
let fetchQuantity = FetchQuantity.specific(10)

do {
let metrics = try await fetcher.getAuthorMetrics(authorID: authorID, fetchQuantity: fetchQuantity)

XCTAssertEqual(metrics.publications, 10, "Total publications should match the requested quantity")
XCTAssertTrue(metrics.citations > 0, "Total citations should be greater than 0")
} catch {
XCTFail("Error fetching author metrics: \(error)")
}
}
}

0 comments on commit a034d04

Please sign in to comment.