//! OpenAlex entity schemas //! //! Represents the core entity types from OpenAlex: //! - Works (publications) //! - Authors //! - Institutions //! - Topics/Concepts //! - Funders //! - Sources (journals, conferences) use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; /// A scholarly work (paper, book, dataset, etc.) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Work { /// OpenAlex ID (e.g., "W2741809807") pub id: String, /// DOI (if available) pub doi: Option, /// Work title pub title: String, /// Publication date pub publication_date: Option>, /// Publication year pub publication_year: Option, /// Work type (article, book, dataset, etc.) #[serde(rename = "type")] pub work_type: Option, /// Open access status pub open_access: Option, /// Citation count pub cited_by_count: u64, /// Authors and their affiliations #[serde(default)] pub authorships: Vec, /// Primary topic pub primary_topic: Option, /// All associated topics #[serde(default)] pub topics: Vec, /// Legacy concepts (deprecated but still in API) #[serde(default)] pub concepts: Vec, /// Referenced works (citations) #[serde(default)] pub referenced_works: Vec, /// Related works #[serde(default)] pub related_works: Vec, /// Abstract (inverted index format in API) pub abstract_inverted_index: Option, /// Publication venue pub primary_location: Option, /// Grants/funding #[serde(default)] pub grants: Vec, /// Bibliographic info pub biblio: Option, /// Last update time pub updated_date: Option>, } /// Open access status #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OpenAccessStatus { /// Is this work open access? pub is_oa: bool, /// OA status type (gold, green, hybrid, bronze) pub oa_status: Option, /// OA URL if available pub oa_url: Option, } /// Author and affiliation information for a work #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Authorship { /// Author position (first, middle, last) pub author_position: AuthorPosition, /// Author details pub author: AuthorReference, /// Institutions at time of publication #[serde(default)] pub institutions: Vec, /// Countries #[serde(default)] pub countries: Vec, /// Is corresponding author #[serde(default)] pub is_corresponding: bool, /// Raw affiliation string pub raw_affiliation_string: Option, } /// Author position in author list #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum AuthorPosition { /// First author First, /// Middle author Middle, /// Last author Last, } /// Reference to an author #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthorReference { /// OpenAlex author ID pub id: String, /// Display name pub display_name: String, /// ORCID (if available) pub orcid: Option, } /// Reference to an institution #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InstitutionReference { /// OpenAlex institution ID pub id: String, /// Display name pub display_name: String, /// Institution type (education, company, etc.) #[serde(rename = "type")] pub institution_type: Option, /// Country code pub country_code: Option, /// ROR ID pub ror: Option, } /// Reference to a topic #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TopicReference { /// OpenAlex topic ID pub id: String, /// Display name pub display_name: String, /// Relevance score (0-1) #[serde(default)] pub score: f64, /// Subfield pub subfield: Option, /// Field pub field: Option, /// Domain pub domain: Option, } /// Reference to a concept (legacy) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConceptReference { /// OpenAlex concept ID pub id: String, /// Display name pub display_name: String, /// Wikidata ID pub wikidata: Option, /// Relevance score #[serde(default)] pub score: f64, /// Hierarchy level (0 = root) #[serde(default)] pub level: u32, } /// Reference to a field/domain #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FieldReference { /// OpenAlex ID pub id: String, /// Display name pub display_name: String, } /// Publication location #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Location { /// Is primary location #[serde(default)] pub is_primary: bool, /// Landing page URL pub landing_page_url: Option, /// PDF URL pub pdf_url: Option, /// Source (journal/conference) pub source: Option, /// License pub license: Option, /// Version pub version: Option, } /// Reference to a source (journal, conference, etc.) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SourceReference { /// OpenAlex source ID pub id: String, /// Display name pub display_name: String, /// ISSN pub issn_l: Option, /// Source type #[serde(rename = "type")] pub source_type: Option, /// Is Open Access journal #[serde(default)] pub is_oa: bool, /// Host organization pub host_organization: Option, } /// Grant/funding information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Grant { /// Funder pub funder: Option, /// Funder display name pub funder_display_name: Option, /// Award ID pub award_id: Option, } /// Reference to a funder #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FunderReference { /// OpenAlex funder ID pub id: String, /// Display name pub display_name: String, } /// Bibliographic details #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Biblio { /// Volume pub volume: Option, /// Issue pub issue: Option, /// First page pub first_page: Option, /// Last page pub last_page: Option, } /// Full author entity #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Author { /// OpenAlex author ID pub id: String, /// ORCID pub orcid: Option, /// Display name pub display_name: String, /// Alternative names #[serde(default)] pub display_name_alternatives: Vec, /// Works count pub works_count: u64, /// Citation count pub cited_by_count: u64, /// H-index pub summary_stats: Option, /// Most recent institution pub last_known_institution: Option, /// All affiliations #[serde(default)] pub affiliations: Vec, /// Topic areas #[serde(default)] pub topics: Vec, /// Works API URL pub works_api_url: Option, /// Updated date pub updated_date: Option>, } /// Author summary statistics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthorStats { /// H-index pub h_index: Option, /// i10-index pub i10_index: Option, /// Two-year mean citedness #[serde(rename = "2yr_mean_citedness")] pub two_year_mean_citedness: Option, } /// Author affiliation #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Affiliation { /// Institution pub institution: InstitutionReference, /// Years affiliated #[serde(default)] pub years: Vec, } /// Full institution entity #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Institution { /// OpenAlex institution ID pub id: String, /// ROR ID pub ror: Option, /// Display name pub display_name: String, /// Country code pub country_code: Option, /// Institution type #[serde(rename = "type")] pub institution_type: Option, /// Homepage URL pub homepage_url: Option, /// Works count pub works_count: u64, /// Citation count pub cited_by_count: u64, /// Geographic info pub geo: Option, /// Parent institutions #[serde(default)] pub lineage: Vec, /// Associated institutions #[serde(default)] pub associated_institutions: Vec, /// Updated date pub updated_date: Option>, } /// Geographic location #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GeoLocation { /// City pub city: Option, /// Region/state pub region: Option, /// Country pub country: Option, /// Country code pub country_code: Option, /// Latitude pub latitude: Option, /// Longitude pub longitude: Option, } /// Full topic entity #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Topic { /// OpenAlex topic ID pub id: String, /// Display name pub display_name: String, /// Description pub description: Option, /// Keywords #[serde(default)] pub keywords: Vec, /// Works count pub works_count: u64, /// Citation count pub cited_by_count: u64, /// Subfield pub subfield: Option, /// Field pub field: Option, /// Domain pub domain: Option, /// Sibling topics #[serde(default)] pub siblings: Vec, /// Updated date pub updated_date: Option>, } /// Legacy concept entity #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Concept { /// OpenAlex concept ID pub id: String, /// Wikidata ID pub wikidata: Option, /// Display name pub display_name: String, /// Description pub description: Option, /// Hierarchy level pub level: u32, /// Works count pub works_count: u64, /// Citation count pub cited_by_count: u64, /// Parent concepts #[serde(default)] pub ancestors: Vec, /// Child concepts #[serde(default)] pub related_concepts: Vec, /// Updated date pub updated_date: Option>, } /// Full source entity #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Source { /// OpenAlex source ID pub id: String, /// ISSN-L pub issn_l: Option, /// All ISSNs #[serde(default)] pub issn: Vec, /// Display name pub display_name: String, /// Publisher pub host_organization: Option, /// Source type (journal, conference, etc.) #[serde(rename = "type")] pub source_type: Option, /// Is Open Access #[serde(default)] pub is_oa: bool, /// Homepage URL pub homepage_url: Option, /// Works count pub works_count: u64, /// Citation count pub cited_by_count: u64, /// Topics #[serde(default)] pub topics: Vec, /// Updated date pub updated_date: Option>, } /// Full funder entity #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Funder { /// OpenAlex funder ID pub id: String, /// Display name pub display_name: String, /// Alternative names #[serde(default)] pub alternate_titles: Vec, /// Country code pub country_code: Option, /// Description pub description: Option, /// Homepage URL pub homepage_url: Option, /// Grants count pub grants_count: u64, /// Works count pub works_count: u64, /// Citation count pub cited_by_count: u64, /// ROR ID pub ror: Option, /// Updated date pub updated_date: Option>, } #[cfg(test)] mod tests { use super::*; #[test] fn test_work_deserialization() { let json = r#"{ "id": "W123", "title": "Test Paper", "cited_by_count": 10, "authorships": [], "topics": [], "concepts": [], "referenced_works": [], "related_works": [], "grants": [] }"#; let work: Work = serde_json::from_str(json).unwrap(); assert_eq!(work.id, "W123"); assert_eq!(work.title, "Test Paper"); assert_eq!(work.cited_by_count, 10); } #[test] fn test_author_position() { let first = serde_json::from_str::(r#""first""#).unwrap(); assert_eq!(first, AuthorPosition::First); let last = serde_json::from_str::(r#""last""#).unwrap(); assert_eq!(last, AuthorPosition::Last); } }