Skip to content

Commit b549c9e

Browse files
authored
fix: tags mapping (#2)
1 parent e182a26 commit b549c9e

File tree

9 files changed

+149
-28
lines changed

9 files changed

+149
-28
lines changed

Cargo.lock

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ serde_json = "1.0.139"
1717
serde = { version = "1.0", features = ["derive"] }
1818
figment = { version = "0.10", features = ["toml", "env"] }
1919
directories = "6.0.0"
20+
chrono = "0.4.39"
2021

2122
[workspace]
2223
members = [ "pkg/localdb",

pkg/localdb/migrations/20250205102710_init.sql

+3-2
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ CREATE TABLE [tags] (
3131
);
3232

3333
CREATE TABLE [items_tags] (
34-
[item_id] INTEGER REFERENCES items(id),
35-
[tag_id] INTEGER REFERENCES tags(id)
34+
[item_id] INTEGER NOT NULL REFERENCES items(id),
35+
[tag_id] INTEGER NOT NULL REFERENCES tags(id),
36+
PRIMARY KEY (tag_id, item_id)
3637
);
3738

3839
CREATE TABLE [authors] (

pkg/localdb/src/db.rs

+102-10
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,41 @@ struct ItemRow {
4242
}
4343

4444
impl LocalDb {
45-
pub fn new(pool: SqlitePool) -> crate::Result<Self> {
46-
Ok(Self { pool })
45+
pub fn new(pool: SqlitePool) -> Self {
46+
Self { pool }
4747
}
4848

49-
pub async fn add(&mut self, item: &item::Item) -> crate::Result<()> {
49+
pub async fn add_tag(&mut self, tag: &item::Tag) -> crate::Result<i32> {
50+
let res = sqlx::query(
51+
"INSERT INTO tags (name, tag) VALUES (?, ?) ON CONFLICT(tag) DO NOTHING RETURNING id",
52+
)
53+
.bind(&tag.name)
54+
.bind(&tag.tag)
55+
.execute(&self.pool)
56+
.await?;
57+
Ok(res.last_insert_rowid() as i32)
58+
}
59+
60+
pub async fn get_tags(&mut self) -> crate::Result<Vec<item::Tag>> {
61+
let res: Vec<item::Tag> = sqlx::query_as("SELECT id, tag, name FROM tags")
62+
.fetch_all(&self.pool)
63+
.await?;
64+
Ok(res)
65+
}
66+
67+
pub async fn link_tag(&mut self, tag: i32, item: i32) -> crate::Result<()> {
5068
sqlx::query(
69+
"INSERT INTO items_tags (item_id, tag_id) VALUES (?, ?) ON CONFLICT(item_id, tag_id) DO NOTHING",
70+
)
71+
.bind(item)
72+
.bind(tag)
73+
.execute(&self.pool)
74+
.await?;
75+
Ok(())
76+
}
77+
78+
pub async fn add(&mut self, item: &item::Item) -> crate::Result<i32> {
79+
let resutl = sqlx::query(
5180
"INSERT INTO items (
5281
pocket_id,
5382
title,
@@ -97,7 +126,16 @@ impl LocalDb {
97126
.bind(item.time_favorited)
98127
.execute(&self.pool)
99128
.await?;
100-
Ok(())
129+
130+
let tem_id = resutl.last_insert_rowid() as i32;
131+
132+
let tags = item.tags.iter().collect_vec();
133+
for tag in tags {
134+
let tag_id = self.add_tag(tag).await?;
135+
self.link_tag(tag_id, tem_id).await?;
136+
}
137+
138+
Ok(tem_id as i32)
101139
}
102140

103141
pub async fn get_items(&self) -> crate::Result<Vec<item::Item>> {
@@ -155,9 +193,18 @@ impl LocalDb {
155193

156194
#[cfg(test)]
157195
mod test {
158-
use crate::item::ItemStatus;
196+
use std::collections::HashSet;
159197

160198
use super::*;
199+
use crate::{
200+
item::{ItemStatus, Tag},
201+
Item,
202+
};
203+
204+
async fn get_db() -> LocalDb {
205+
let pool = open_database(":memory:").await.unwrap();
206+
LocalDb::new(pool)
207+
}
161208

162209
#[tokio::test]
163210
async fn test_open_in_memory() {
@@ -167,23 +214,68 @@ mod test {
167214

168215
#[tokio::test]
169216
async fn test_get_items() {
170-
let pool = open_database(":memory:").await.unwrap();
171-
let db = LocalDb::new(pool).unwrap();
217+
let db = get_db().await;
172218
let items = db.get_items().await.unwrap();
173219
assert!(items.is_empty());
174220
}
175221

176222
#[tokio::test]
177223
async fn test_add_item() {
178-
let pool = open_database(":memory:").await.unwrap();
179-
let mut db = LocalDb::new(pool).unwrap();
180-
let item = Default::default();
224+
let mut db = get_db().await;
225+
226+
let tag = Tag {
227+
id: 0,
228+
tag: "tag".to_string(),
229+
name: None,
230+
};
231+
232+
let item = Item {
233+
tags: HashSet::from([Tag::default(), tag]),
234+
..Default::default()
235+
};
236+
181237
db.add(&item).await.unwrap();
238+
182239
let items = db.get_items().await.unwrap();
183240
assert_eq!(items.len(), 1);
184241
assert_eq!(items[0].title, item.title);
185242
assert_eq!(items[0].url, item.url);
186243
assert_eq!(items[0].id, 1);
187244
assert_eq!(items[0].status, ItemStatus::Unread);
245+
246+
assert_eq!(items[0].tags.len(), 2);
247+
assert!(items[0].tags.iter().any(|t| t.tag == "tag"));
248+
assert!(items[0].tags.iter().any(|t| t.tag == "example"));
249+
}
250+
251+
#[tokio::test]
252+
async fn test_add_tag() {
253+
let mut db = get_db().await;
254+
let result = db.add_tag(&Default::default()).await;
255+
assert!(result.is_ok());
256+
assert_eq!(result.unwrap(), 1);
257+
}
258+
259+
#[tokio::test]
260+
async fn test_add_duplicate_tag() {
261+
let mut db = get_db().await;
262+
db.add_tag(&Default::default())
263+
.await
264+
.expect("add_tag failed");
265+
db.add_tag(&Default::default())
266+
.await
267+
.expect("add tag failed");
268+
269+
let tags = db.get_tags().await.unwrap();
270+
assert_eq!(tags.len(), 1);
271+
}
272+
273+
#[tokio::test]
274+
async fn test_link_tag() {
275+
let mut db = get_db().await;
276+
let tag_id = db.add_tag(&Tag::default()).await.unwrap();
277+
let item_id = db.add(&Default::default()).await.unwrap();
278+
let res = db.link_tag(tag_id, item_id).await;
279+
assert!(res.is_ok());
188280
}
189281
}

pkg/localdb/src/item.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ impl Default for Item {
8080
time_read: None,
8181
time_favorited: None,
8282
tags: HashSet::new(),
83-
8483
is_article: None,
8584
is_index: None,
8685
has_video: None,
@@ -101,6 +100,16 @@ pub struct Tag {
101100
pub name: Option<String>,
102101
}
103102

103+
impl Default for Tag {
104+
fn default() -> Self {
105+
Tag {
106+
id: 0,
107+
tag: "example".to_string(),
108+
name: Some("Example Tag".to_string()),
109+
}
110+
}
111+
}
112+
104113
#[derive(Deserialize, Debug, sqlx::Type, PartialEq, Eq, Clone, Copy)]
105114
#[repr(i32)]
106115
pub enum ItemStatus {

pkg/pocket/src/error.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ pub enum PocketError {
99
#[error("Request error for URL <{url}>: {source}")]
1010
Reqwest { url: String, source: reqwest::Error },
1111
#[error("Pocket protocol error: {1} ({0})")]
12-
Proto(String, String, Option<String>),
12+
Proto(i32, String, Option<String>),
1313
#[error("X-Error-Code is malformed UTF-8")]
1414
ReqwwestStrError(#[from] reqwest::header::ToStrError),
1515

pkg/pocket/src/lib.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,12 @@ impl<'a> PocketClient<'a> {
7171
})?;
7272

7373
if let Some(code) = res.headers().get(X_ERROR_CODE) {
74-
let code = code.to_str().expect("X-Error-Code is malformed").into();
74+
let code = code
75+
.to_str()
76+
.expect("X-Error-Code is malformed")
77+
.parse()
78+
.expect("X-Error-Code is malformed integer");
79+
7580
return Err(PocketError::Proto(
7681
code,
7782
res.headers()

src/config.rs

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub fn get_config() -> anyhow::Result<Config> {
2121
let config_dir = project_dirs.config_dir();
2222
std::fs::create_dir_all(project_dirs.config_local_dir())?;
2323
std::fs::create_dir_all(project_dirs.data_local_dir())?;
24+
std::fs::File::open(project_dirs.data_local_dir().join("readlater.sqlite"))?;
2425

2526
let config_file = config_dir.join("config.toml");
2627
let config: Config = Figment::new()

src/main.rs

+22-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use std::{collections::HashMap, path::PathBuf};
2-
1+
use chrono::DateTime;
32
use clap::{Parser, Subcommand};
43
use localdb::KvDB;
54
use pocket::{modify::AddUrlRequest, GetOptions, PocketClient};
@@ -10,6 +9,7 @@ use readlater::{
109
native_host_handler,
1110
},
1211
};
12+
use std::{collections::HashMap, path::PathBuf};
1313
use url::Url;
1414
#[derive(Parser)]
1515
#[command(author, version, about)]
@@ -87,19 +87,29 @@ async fn main() {
8787
)
8888
.await
8989
.unwrap();
90-
let mut db = localdb::LocalDb::new(pool.clone()).unwrap();
90+
let mut db = localdb::LocalDb::new(pool.clone());
9191
let mut kv_db = KvDB::new(pool.clone());
9292
let since = kv_db
9393
.get_kv::<i32>("pocket_since")
9494
.await
9595
.map(|k| k.value)
96-
.unwrap_or(0);
96+
.ok();
9797

98-
let mut offset = kv_db
98+
let offset = kv_db
9999
.get_kv::<i32>("pocket_offset")
100100
.await
101101
.map(|k| k.value)
102-
.unwrap_or(0);
102+
.ok();
103+
104+
let (since, mut offset) = match (since, offset) {
105+
(Some(since), _) => (since, 0),
106+
(_, Some(offset)) => (0, offset),
107+
_ => (0, 0),
108+
};
109+
110+
let datetime =
111+
DateTime::from_timestamp(since as i64, 0).expect("unexpected date time");
112+
println!("Syncing data since {} with offset {}", datetime, offset);
103113

104114
loop {
105115
let response = pocket
@@ -112,21 +122,20 @@ async fn main() {
112122
.await
113123
.unwrap();
114124

115-
offset += 30;
116-
kv_db
117-
.set_kv(&("pocket_offset", offset).into())
118-
.await
119-
.unwrap();
120-
121125
for article in response.list.values() {
122126
let article: localdb::Item = article.into();
123127
db.add(&article).await.unwrap();
124128
println!("{} {}", article.id, article.title);
125129
}
126130

131+
offset += 30;
132+
kv_db
133+
.set_kv(&("pocket_offset", offset).into())
134+
.await
135+
.unwrap();
136+
127137
let has_more = response.has_more().expect("invalid request");
128138
if response.list.is_empty() || !has_more {
129-
println!("Since {}", response.since);
130139
kv_db
131140
.set_kv(
132141
&("pocket_since".to_string(), response.since.to_string())

0 commit comments

Comments
 (0)