aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jake Swenson <jakeswenson@users.noreply.github.com> 2021-05-23 10:31:38 -0700
committerGravatar GitHub <noreply@github.com> 2021-05-23 10:31:38 -0700
commit9d5b84b88ddc2b7de1bc0f03d9026eb52e8976ac (patch)
tree735b1031148d4cddfa1a0e01f52b02e23e94f9cb
parent4cffa16633f83b6604f7ce3de47d40a29e6a9b54 (diff)
downloadnotion-9d5b84b88ddc2b7de1bc0f03d9026eb52e8976ac.tar.gz
notion-9d5b84b88ddc2b7de1bc0f03d9026eb52e8976ac.tar.zst
notion-9d5b84b88ddc2b7de1bc0f03d9026eb52e8976ac.zip
Add initial todo example (#13)
-rw-r--r--.github/workflows/build.yml2
-rw-r--r--.gitignore1
-rw-r--r--Cargo.toml9
-rw-r--r--README.md8
-rw-r--r--examples/todo/README.md19
-rw-r--r--examples/todo/commands.rs1
-rw-r--r--examples/todo/commands/configure.rs70
-rw-r--r--examples/todo/main.rs76
-rw-r--r--src/models.rs28
-rw-r--r--src/models/search.rs9
-rw-r--r--src/models/text.rs11
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:
diff --git a/.gitignore b/.gitignore
index 71f847c..47a0031 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@ Cargo.lock
**/*.rs.bk
.api_token
+todo_config.toml
diff --git a/Cargo.toml b/Cargo.toml
index c1ddd6b..07e9d53 100644
--- a/Cargo.toml
+++ b/Cargo.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"]
diff --git a/README.md b/README.md
index c27cadc..9682777 100644
--- a/README.md
+++ b/README.md
@@ -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
+ }
+ }
+ }
+}