diff options
author | 2021-05-23 10:31:38 -0700 | |
---|---|---|
committer | 2021-05-23 10:31:38 -0700 | |
commit | 9d5b84b88ddc2b7de1bc0f03d9026eb52e8976ac (patch) | |
tree | 735b1031148d4cddfa1a0e01f52b02e23e94f9cb | |
parent | 4cffa16633f83b6604f7ce3de47d40a29e6a9b54 (diff) | |
download | notion-9d5b84b88ddc2b7de1bc0f03d9026eb52e8976ac.tar.gz notion-9d5b84b88ddc2b7de1bc0f03d9026eb52e8976ac.tar.zst notion-9d5b84b88ddc2b7de1bc0f03d9026eb52e8976ac.zip |
Add initial todo example (#13)
-rw-r--r-- | .github/workflows/build.yml | 2 | ||||
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Cargo.toml | 9 | ||||
-rw-r--r-- | README.md | 8 | ||||
-rw-r--r-- | examples/todo/README.md | 19 | ||||
-rw-r--r-- | examples/todo/commands.rs | 1 | ||||
-rw-r--r-- | examples/todo/commands/configure.rs | 70 | ||||
-rw-r--r-- | examples/todo/main.rs | 76 | ||||
-rw-r--r-- | src/models.rs | 28 | ||||
-rw-r--r-- | src/models/search.rs | 9 | ||||
-rw-r--r-- | src/models/text.rs | 11 |
11 files changed, 233 insertions, 1 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd325ac..c1dd3e9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,7 +35,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: build - args: --all-features + args: --all-targets - name: Run tests uses: actions-rs/cargo@v1 env: @@ -10,3 +10,4 @@ Cargo.lock **/*.rs.bk .api_token +todo_config.toml @@ -32,3 +32,12 @@ features = ["derive"] [dev-dependencies] cargo-husky = "1" wiremock = "0.5.2" +anyhow = "1.0.40" +clap = "3.0.0-beta.2" +skim = "0.9.4" +crossbeam-channel = "0.5" +toml = "0.5.8" + +[dev-dependencies.config] +version = "0.11.0" +features = ["toml"] @@ -4,3 +4,11 @@ Notion API client library for rust. +This project is under active development and this README will be updated as this library gets closer to a reliable state. +However, if you're really eager see the example todo cli application provided in [examples/todo](examples/todo). + +## Building + +```bash +cargo build +``` diff --git a/examples/todo/README.md b/examples/todo/README.md new file mode 100644 index 0000000..b22d08b --- /dev/null +++ b/examples/todo/README.md @@ -0,0 +1,19 @@ +# Notion database todo example + +This example is builds a todo list using a notion database. + +## Setup your notion api token + +Create an `internal` integration here: https://www.notion.so/my-integrations + +```bash + export NOTION_API_TOKEN='secret_token_here' +``` +> Notice the space before the export command. +> This will prevent your terminal from storing this token in your shell history... + +## Selecting the database to use + +```bash +cargo run --example todo -- config +``` diff --git a/examples/todo/commands.rs b/examples/todo/commands.rs new file mode 100644 index 0000000..a78a393 --- /dev/null +++ b/examples/todo/commands.rs @@ -0,0 +1 @@ +pub mod configure; diff --git a/examples/todo/commands/configure.rs b/examples/todo/commands/configure.rs new file mode 100644 index 0000000..42ffafd --- /dev/null +++ b/examples/todo/commands/configure.rs @@ -0,0 +1,70 @@ +use crate::TodoConfig; +use anyhow::Result; +use notion::models::search::NotionSearch; +use notion::models::{Database, DatabaseId}; +use notion::{AsIdentifier, NotionApi}; +use skim::{Skim, SkimItem, SkimItemReceiver, SkimItemSender, SkimOptions}; +use std::borrow::Cow; +use std::ops::Deref; +use std::sync::Arc; + +fn skim_select_database(databases: Vec<Database>) -> Result<DatabaseId> { + let options = SkimOptions::default(); + + let (sender, receiver): (SkimItemSender, SkimItemReceiver) = crossbeam_channel::bounded(500); + + struct SkimDB { + db: Database, + } + + impl SkimItem for SkimDB { + fn text(&self) -> Cow<str> { + Cow::Owned(self.db.title_plain_text()) + } + } + + for db in databases { + sender.send(Arc::new(SkimDB { db }))?; + } + + // `run_with` would read and show items from the stream + let selected_items = Skim::run_with(&options, Some(receiver)) + .filter(|out| !out.is_abort) + .map(|out| out.selected_items) + .unwrap_or_else(|| Vec::new()); + + let db = selected_items + .first() + .expect("No database selected, aborting...") + .clone(); + let db: &SkimDB = db + .deref() + .as_any() + .downcast_ref() + .expect("Couldn't cast back to SkimDB"); + + let database_id = db.db.id(); + + Ok(database_id) +} + +pub async fn configure(notion_api: NotionApi) -> Result<()> { + let databases: Vec<Database> = notion_api + .search(NotionSearch::filter_by_databases()) + .await? + .only_databases() + .results; + + let database_id = skim_select_database(databases)?; + + println!("Selected database's id: {}", database_id); + + let bytes = toml::to_vec(&TodoConfig { + api_token: None, + task_database_id: Some(database_id), + })?; + + std::fs::write("../todo_config.toml", bytes)?; + + Ok(()) +} diff --git a/examples/todo/main.rs b/examples/todo/main.rs new file mode 100644 index 0000000..2283549 --- /dev/null +++ b/examples/todo/main.rs @@ -0,0 +1,76 @@ +mod commands; + +use anyhow::{Context, Result}; +use clap::Clap; +use notion::models::DatabaseId; +use notion::NotionApi; +use serde::{Deserialize, Serialize}; + +// https://docs.rs/clap/3.0.0-beta.2/clap/ +#[derive(Clap)] +#[clap(version = "1.0", author = "Kevin K. <kbknapp@gmail.com>")] +struct Opts { + #[clap(subcommand)] + command: SubCommand, +} + +#[derive(Clap)] +enum SubCommand { + /// Configure what database this notion-todo example uses + Config, + /// List all todos + List, + /// Add a todo item to the notion database + Add, + /// Complete a todo item + Check, +} + +#[derive(Deserialize, Serialize)] +struct TodoConfig { + api_token: Option<String>, + task_database_id: Option<DatabaseId>, +} + +#[tokio::main] +async fn main() -> Result<()> { + let opts: Opts = Opts::parse(); + + // https://docs.rs/config/0.11.0/config/ + let config = config::Config::default() + .with_merged(config::File::with_name("todo_config")) + .unwrap_or_default() + .with_merged(config::Environment::with_prefix("NOTION"))?; + + let config: TodoConfig = config.try_into().context("Failed to read config")?; + + let notion_api = NotionApi::new( + std::env::var("NOTION_API_TOKEN") + .or(config + .api_token + .ok_or(anyhow::anyhow!("No api token from config"))) + .context( + "No Notion API token found in either the environment variable \ + `NOTION_API_TOKEN` or the config file!", + )?, + )?; + + match opts.command { + SubCommand::Config => commands::configure::configure(notion_api).await, + SubCommand::List => list_tasks(notion_api), + SubCommand::Add => add_task(notion_api), + SubCommand::Check => complete_task(notion_api), + } +} + +fn list_tasks(_notion_api: NotionApi) -> Result<()> { + Ok(()) +} + +fn add_task(_notion_api: NotionApi) -> Result<()> { + Ok(()) +} + +fn complete_task(_notion_api: NotionApi) -> Result<()> { + Ok(()) +} diff --git a/src/models.rs b/src/models.rs index 82ef7c9..78ad105 100644 --- a/src/models.rs +++ b/src/models.rs @@ -66,6 +66,15 @@ impl AsIdentifier<DatabaseId> for Database { } } +impl Database { + pub fn title_plain_text(&self) -> String { + self.title + .iter() + .flat_map(|rich_text| rich_text.plain_text().chars()) + .collect() + } +} + /// https://developers.notion.com/reference/pagination#responses #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] pub struct ListResponse<T> { @@ -80,6 +89,25 @@ impl<T> ListResponse<T> { } } +impl ListResponse<Object> { + pub fn only_databases(self) -> ListResponse<Database> { + let databases = self + .results + .into_iter() + .filter_map(|object| match object { + Object::Database { database } => Some(database), + _ => None, + }) + .collect(); + + ListResponse { + results: databases, + has_more: self.has_more, + next_cursor: self.next_cursor, + } + } +} + /// A zero-cost wrapper type around a Page ID #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Clone)] #[serde(transparent)] diff --git a/src/models/search.rs b/src/models/search.rs index 6de3c8a..ffcac1d 100644 --- a/src/models/search.rs +++ b/src/models/search.rs @@ -310,6 +310,15 @@ pub enum NotionSearch { }, } +impl NotionSearch { + pub fn filter_by_databases() -> Self { + Self::Filter { + property: FilterProperty::Object, + value: FilterValue::Database, + } + } +} + impl From<NotionSearch> for SearchRequest { fn from(search: NotionSearch) -> Self { match search { diff --git a/src/models/text.rs b/src/models/text.rs index a10007d..f7ea2b6 100644 --- a/src/models/text.rs +++ b/src/models/text.rs @@ -81,3 +81,14 @@ pub enum RichText { rich_text: RichTextCommon, }, } + +impl RichText { + pub fn plain_text(&self) -> &str { + use RichText::*; + match self { + Text { rich_text, .. } | Mention { rich_text, .. } | Equation { rich_text, .. } => { + &rich_text.plain_text + } + } + } +} |