Skip to content

Commit

Permalink
Return CSL-JSON format
Browse files Browse the repository at this point in the history
  • Loading branch information
Maarrk committed May 21, 2022
1 parent f4e32f2 commit bfd3884
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 4 deletions.
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "gibcite"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
authors = ["Marek S. Łukasiewicz <marek@lukasiewicz.tech>"]
description = "Give details of citation from local Zotero database"
Expand All @@ -17,6 +17,7 @@ categories = ["command-line-utilities"]
clap = { version = "3.1.18", features = ["derive"] }
directories = "4.0"
exitcode = "1.1.2"
regex = "1"
rusqlite = { version = "0.27.0", features = ["backup", "bundled"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Expand Down
64 changes: 64 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use directories::UserDirs;
use regex::Regex;
use rusqlite::Connection;
use serde::Deserialize;
use serde_json::Value;
use std::{error::Error, path::PathBuf};

pub mod output;

pub fn get_database_path(input: &Option<PathBuf>) -> Result<Box<PathBuf>, String> {
let path_buf = match input {
Some(path) => path.to_owned(),
Expand Down Expand Up @@ -118,6 +121,30 @@ pub fn get_field(
Ok(row)
}

pub fn get_date_parts(
conn: &Connection,
item_id: i64,
) -> Result<(i32, Option<i32>, Option<i32>), Box<dyn Error>> {
let re = Regex::new(r"^(\d{4})-(\d{2})-(\d{2})")?;
let date_text = get_field(&conn, item_id, "date")?;
if let Some(caps) = re.captures(&date_text) {
// Parsing or index access won't panic since it matched the regex
let year = caps[1].parse().unwrap();
let month = caps[2].parse().unwrap();
let day = caps[3].parse().unwrap();
Ok((
year,
if month != 0 { Some(month) } else { None },
if day != 0 { Some(day) } else { None },
))
} else {
Err(format!(
"didn't match '{}' to expected YYYY-mm-dd format",
date_text
))?
}
}

pub mod test_utils {
use rusqlite::{Batch, Connection};
use std::error::Error;
Expand Down Expand Up @@ -200,4 +227,41 @@ mod tests {
get_field(&conn, 1, "abstractNote").unwrap()
);
}

#[test]
fn mock_date() {
let conn = setup_database().unwrap();
assert_eq!(
(2022, Some(05), Some(15)),
get_date_parts(&conn, 1).unwrap()
);
}

#[test]
fn missing_day() {
let conn = setup_database().unwrap();
conn.execute(
"
UPDATE itemDataValues
SET value = '2022-05-00'
WHERE valueID = 3;",
[],
)
.unwrap();
assert_eq!((2022, Some(05), None), get_date_parts(&conn, 1).unwrap());
}

#[test]
fn missing_month() {
let conn = setup_database().unwrap();
conn.execute(
"
UPDATE itemDataValues
SET value = '2022-00-00'
WHERE valueID = 3;",
[],
)
.unwrap();
assert_eq!((2022, None, None), get_date_parts(&conn, 1).unwrap());
}
}
11 changes: 9 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use rusqlite::{Connection, OpenFlags};
use std::path::PathBuf;
use std::process::exit;

use gibcite::{count_items, get_database_path, get_field, get_item_id};
use gibcite::output::csl_json;
use gibcite::{count_items, get_database_path, get_item_id};

#[derive(Debug, Parser)]
#[clap(author, version, about, long_about = None)]
Expand Down Expand Up @@ -63,6 +64,12 @@ fn main() {
exit(exitcode::DATAERR);
});

println!("{}", get_field(&conn, item_id, "title").unwrap());
println!(
"{}",
csl_json(&conn, &cli.citation_key, item_id).unwrap_or_else(|err| {
eprintln!("could not output CSL: {}", err);
exit(exitcode::SOFTWARE);
})
);
exit(exitcode::OK);
}
142 changes: 142 additions & 0 deletions src/output.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use std::error::Error;

use crate::{get_creators, get_date_parts, get_field};

#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct CSLItem {
pub id: String,
#[serde(rename = "abstract")]
pub abstract_note: String,
pub title: String,
pub author: Vec<CSLAuthor>,
#[serde(rename = "citation-key")]
pub citation_key: String,
pub issued: CSLDate,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct CSLAuthor {
pub family: String,
pub given: String,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct CSLDate {
#[serde(rename = "date-parts")]
pub date_parts: Vec<Vec<i32>>,
}

impl CSLDate {
fn new(date: (i32, Option<i32>, Option<i32>)) -> Self {
let mut parts = vec![date.0];
if let Some(m) = date.1 {
parts.push(m);
if let Some(d) = date.2 {
parts.push(d);
}
}
Self {
date_parts: vec![parts],
}
}
}

fn make_csl(
conn: &Connection,
citation_key: &str,
item_id: i64,
) -> Result<CSLItem, Box<dyn Error>> {
Ok(CSLItem {
id: citation_key.into(),
abstract_note: get_field(&conn, item_id, "abstractNote")?,
title: get_field(&conn, item_id, "title")?,
author: get_creators(&conn, item_id)?
.iter()
.map(|(first, last)| CSLAuthor {
family: last.into(),
given: first.into(),
})
.collect(),
citation_key: citation_key.into(),
issued: CSLDate::new(get_date_parts(&conn, item_id)?),
})
}

pub fn csl_json(
conn: &Connection,
citation_key: &str,
item_id: i64,
) -> Result<String, Box<dyn Error>> {
let serialized = serde_json::to_string(&vec![make_csl(&conn, citation_key, item_id)?])?;
Ok(serialized)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;

fn mock_item() -> CSLItem {
CSLItem {
id: "kowalskiSampleTitle2022".into(),
abstract_note: "Abstract for a sample article".into(),
title: "A sample title".into(),
author: vec![CSLAuthor {
family: "Kowalski".into(),
given: "Jan".into(),
}],
citation_key: "kowalskiSampleTitle2022".into(),
issued: CSLDate::new((2022, Some(5), Some(15))),
}
}

#[test]
fn mock_struct() {
let conn = setup_database().unwrap();
let citation_key = "kowalskiSampleTitle2022";
let item_id = 1;

assert_eq!(mock_item(), make_csl(&conn, citation_key, item_id).unwrap());
}

#[test]
fn example_json() {
let conn = setup_database().unwrap();
let citation_key = "kowalskiSampleTitle2022";
let item_id = 1;

let csl_example = r#"
{
"id": "kowalskiSampleTitle2022",
"abstract": "Abstract for a sample article",
"container-title": "",
"DOI": "",
"event": "",
"page": "",
"source": "",
"title": "A sample title",
"author": [
{
"family": "Kowalski",
"given": "Jan"
}
],
"issued": {
"date-parts": [
[
2022,
5,
15
]
]
},
"citation-key": "kowalskiSampleTitle2022"
}"#;
assert_eq!(
serde_json::from_str::<CSLItem>(csl_example).unwrap(),
make_csl(&conn, citation_key, item_id).unwrap()
)
}
}

0 comments on commit bfd3884

Please sign in to comment.