notra 0.1.1

Unofficial Rust SDK for the Notra API
Documentation
use serde::{Deserialize, Serialize};

use crate::client::Notra;
use crate::error::Result;
use crate::models::{
    ContentType, DataPoints, LookbackWindow, Organization, Pagination, Post, PostStatus,
    Repository, SelectedItems, Sort,
};

// ── List Posts ──────────────────────────────────────────────────────────

#[derive(Debug, Deserialize)]
pub struct ListPostsResponse {
    pub organization: Organization,
    pub posts: Vec<Post>,
    pub pagination: Pagination,
}

pub struct ListPostsBuilder<'a> {
    client: &'a Notra,
    sort: Option<Sort>,
    limit: Option<u32>,
    page: Option<u32>,
    status: Option<PostStatus>,
    content_type: Option<ContentType>,
}

impl<'a> ListPostsBuilder<'a> {
    pub(crate) fn new(client: &'a Notra) -> Self {
        Self {
            client,
            sort: None,
            limit: None,
            page: None,
            status: None,
            content_type: None,
        }
    }

    pub fn sort(mut self, sort: Sort) -> Self {
        self.sort = Some(sort);
        self
    }

    pub fn limit(mut self, limit: u32) -> Self {
        self.limit = Some(limit);
        self
    }

    pub fn page(mut self, page: u32) -> Self {
        self.page = Some(page);
        self
    }

    pub fn status(mut self, status: PostStatus) -> Self {
        self.status = Some(status);
        self
    }

    pub fn content_type(mut self, content_type: ContentType) -> Self {
        self.content_type = Some(content_type);
        self
    }

    pub async fn send(self) -> Result<ListPostsResponse> {
        let mut query: Vec<(&str, String)> = Vec::new();
        if let Some(sort) = self.sort {
            query.push(("sort", serde_json::to_value(sort).unwrap().as_str().unwrap().to_string()));
        }
        if let Some(limit) = self.limit {
            query.push(("limit", limit.to_string()));
        }
        if let Some(page) = self.page {
            query.push(("page", page.to_string()));
        }
        if let Some(status) = self.status {
            query.push(("status", serde_json::to_value(status).unwrap().as_str().unwrap().to_string()));
        }
        if let Some(ct) = self.content_type {
            query.push(("contentType", serde_json::to_value(ct).unwrap().as_str().unwrap().to_string()));
        }
        self.client.get("/v1/posts", &query).await
    }
}

// ── Get Post ────────────────────────────────────────────────────────────

#[derive(Debug, Deserialize)]
pub struct GetPostResponse {
    pub organization: Organization,
    pub post: Option<Post>,
}

pub struct GetPostBuilder<'a> {
    client: &'a Notra,
    post_id: String,
}

impl<'a> GetPostBuilder<'a> {
    pub(crate) fn new(client: &'a Notra, post_id: impl Into<String>) -> Self {
        Self {
            client,
            post_id: post_id.into(),
        }
    }

    pub async fn send(self) -> Result<GetPostResponse> {
        let path = format!("/v1/posts/{}", self.post_id);
        self.client.get(&path, &[]).await
    }
}

// ── Create Post Generation ──────────────────────────────────────────────

#[derive(Debug, Deserialize)]
pub struct CreatePostGenerationResponse {
    pub organization: Organization,
    pub job: crate::models::PostGenerationJob,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CreatePostGenerationBody {
    content_type: ContentType,
    #[serde(skip_serializing_if = "Option::is_none")]
    lookback_window: Option<LookbackWindow>,
    #[serde(skip_serializing_if = "Option::is_none")]
    brand_voice_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    brand_identity_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    git_hub_integration_ids: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    linear_integration_ids: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    repositories: Option<Vec<Repository>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    data_points: Option<DataPoints>,
    #[serde(skip_serializing_if = "Option::is_none")]
    selected_items: Option<SelectedItems>,
}

pub struct CreatePostGenerationBuilder<'a> {
    client: &'a Notra,
    content_type: ContentType,
    lookback_window: Option<LookbackWindow>,
    brand_voice_id: Option<String>,
    brand_identity_id: Option<String>,
    github_integration_ids: Option<Vec<String>>,
    linear_integration_ids: Option<Vec<String>>,
    repositories: Option<Vec<Repository>>,
    data_points: Option<DataPoints>,
    selected_items: Option<SelectedItems>,
}

impl<'a> CreatePostGenerationBuilder<'a> {
    pub(crate) fn new(client: &'a Notra, content_type: ContentType) -> Self {
        Self {
            client,
            content_type,
            lookback_window: None,
            brand_voice_id: None,
            brand_identity_id: None,
            github_integration_ids: None,
            linear_integration_ids: None,
            repositories: None,
            data_points: None,
            selected_items: None,
        }
    }

    pub fn lookback_window(mut self, window: LookbackWindow) -> Self {
        self.lookback_window = Some(window);
        self
    }

    pub fn brand_voice_id(mut self, id: impl Into<String>) -> Self {
        self.brand_voice_id = Some(id.into());
        self
    }

    pub fn brand_identity_id(mut self, id: impl Into<String>) -> Self {
        self.brand_identity_id = Some(id.into());
        self
    }

    pub fn github_integration_ids(mut self, ids: Vec<String>) -> Self {
        self.github_integration_ids = Some(ids);
        self
    }

    pub fn linear_integration_ids(mut self, ids: Vec<String>) -> Self {
        self.linear_integration_ids = Some(ids);
        self
    }

    pub fn repositories(mut self, repos: Vec<Repository>) -> Self {
        self.repositories = Some(repos);
        self
    }

    pub fn data_points(mut self, data_points: DataPoints) -> Self {
        self.data_points = Some(data_points);
        self
    }

    pub fn selected_items(mut self, items: SelectedItems) -> Self {
        self.selected_items = Some(items);
        self
    }

    pub async fn send(self) -> Result<CreatePostGenerationResponse> {
        let body = CreatePostGenerationBody {
            content_type: self.content_type,
            lookback_window: self.lookback_window,
            brand_voice_id: self.brand_voice_id,
            brand_identity_id: self.brand_identity_id,
            git_hub_integration_ids: self.github_integration_ids,
            linear_integration_ids: self.linear_integration_ids,
            repositories: self.repositories,
            data_points: self.data_points,
            selected_items: self.selected_items,
        };
        self.client.post("/v1/posts/generate", &body).await
    }
}

// ── Get Post Generation ─────────────────────────────────────────────────

#[derive(Debug, Deserialize)]
pub struct GetPostGenerationResponse {
    pub organization: Organization,
    pub job: crate::models::PostGenerationJob,
    #[serde(default)]
    pub events: Vec<crate::models::JobEvent>,
}

pub struct GetPostGenerationBuilder<'a> {
    client: &'a Notra,
    job_id: String,
}

impl<'a> GetPostGenerationBuilder<'a> {
    pub(crate) fn new(client: &'a Notra, job_id: impl Into<String>) -> Self {
        Self {
            client,
            job_id: job_id.into(),
        }
    }

    pub async fn send(self) -> Result<GetPostGenerationResponse> {
        let path = format!("/v1/posts/generate/{}", self.job_id);
        self.client.get(&path, &[]).await
    }
}

// ── Update Post ─────────────────────────────────────────────────────────

#[derive(Debug, Deserialize)]
pub struct UpdatePostResponse {
    pub organization: Organization,
    pub post: Post,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct UpdatePostBody {
    #[serde(skip_serializing_if = "Option::is_none")]
    title: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    slug: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    markdown: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    status: Option<PostStatus>,
}

pub struct UpdatePostBuilder<'a> {
    client: &'a Notra,
    post_id: String,
    title: Option<String>,
    slug: Option<String>,
    markdown: Option<String>,
    status: Option<PostStatus>,
}

impl<'a> UpdatePostBuilder<'a> {
    pub(crate) fn new(client: &'a Notra, post_id: impl Into<String>) -> Self {
        Self {
            client,
            post_id: post_id.into(),
            title: None,
            slug: None,
            markdown: None,
            status: None,
        }
    }

    pub fn title(mut self, title: impl Into<String>) -> Self {
        self.title = Some(title.into());
        self
    }

    pub fn slug(mut self, slug: impl Into<String>) -> Self {
        self.slug = Some(slug.into());
        self
    }

    pub fn markdown(mut self, markdown: impl Into<String>) -> Self {
        self.markdown = Some(markdown.into());
        self
    }

    pub fn status(mut self, status: PostStatus) -> Self {
        self.status = Some(status);
        self
    }

    pub async fn send(self) -> Result<UpdatePostResponse> {
        let path = format!("/v1/posts/{}", self.post_id);
        let body = UpdatePostBody {
            title: self.title,
            slug: self.slug,
            markdown: self.markdown,
            status: self.status,
        };
        self.client.patch(&path, &body).await
    }
}

// ── Delete Post ─────────────────────────────────────────────────────────

#[derive(Debug, Deserialize)]
pub struct DeletePostResponse {
    pub id: String,
    pub organization: Organization,
}

pub struct DeletePostBuilder<'a> {
    client: &'a Notra,
    post_id: String,
}

impl<'a> DeletePostBuilder<'a> {
    pub(crate) fn new(client: &'a Notra, post_id: impl Into<String>) -> Self {
        Self {
            client,
            post_id: post_id.into(),
        }
    }

    pub async fn send(self) -> Result<DeletePostResponse> {
        let path = format!("/v1/posts/{}", self.post_id);
        self.client.delete(&path).await
    }
}