port-sdk 0.1.0

Rust SDK for Port APIs.
Documentation
//! High-level API wrappers built on top of `PortClient`.
//!
//! This module will be split into submodules (e.g., `blueprints`, `entities`) that map to the
//! resources exposed by the Port REST API. Each function should accept typed request payloads,
//! delegate to `PortClient`, and return typed responses from `crate::types`.

use crate::client::{Pagination, PortClient};
use crate::error::PortError;
use crate::types::filters::FilterBuilder;
use crate::types::IdentifiedResource;
use serde::de::DeserializeOwned;
use serde::Serialize;

#[derive(Serialize)]
struct EmptyQuery;

#[inline]
fn build_path<I, S>(segments: I) -> String
where
    I: IntoIterator<Item = S>,
    S: AsRef<str>,
{
    let mut path = String::new();
    for segment in segments {
        let trimmed = segment.as_ref().trim_matches('/');
        if trimmed.is_empty() {
            continue;
        }
        if !path.is_empty() {
            path.push('/');
        }
        path.push_str(trimmed);
    }
    path
}

#[inline]
async fn list_helper<R, Q>(
    client: &PortClient,
    path: &str,
    query: Option<&Q>,
    pagination: Option<&Pagination>,
) -> Result<R, PortError>
where
    R: DeserializeOwned,
    Q: Serialize + ?Sized,
{
    match (query, pagination) {
        (Some(q), Some(p)) => client.get_paginated(path, q, p).await,
        (Some(q), None) => client.get_with_query(path, q).await,
        (None, Some(p)) => client.get_paginated(path, &EmptyQuery, p).await,
        (None, None) => client.get(path).await,
    }
}

/// Blueprint-related helpers built on top of [`PortClient`].
pub mod blueprints {
    use super::*;
    use crate::types::blueprints::BlueprintDefinition;

    const RESOURCE: &str = "blueprints";

    /// Fetch all blueprints with optional pagination.
    pub async fn list<R>(
        client: &PortClient,
        pagination: Option<&Pagination>,
    ) -> Result<R, PortError>
    where
        R: DeserializeOwned,
    {
        let path = build_path([RESOURCE]);
        list_helper(client, &path, None::<&EmptyQuery>, pagination).await
    }

    /// Fetch blueprints and apply ad-hoc filters.
    pub async fn list_with_query<R, Q>(
        client: &PortClient,
        query: &Q,
        pagination: Option<&Pagination>,
    ) -> Result<R, PortError>
    where
        R: DeserializeOwned,
        Q: Serialize + ?Sized,
    {
        let path = build_path([RESOURCE]);
        list_helper(client, &path, Some(query), pagination).await
    }

    pub async fn get<R>(client: &PortClient, identifier: impl AsRef<str>) -> Result<R, PortError>
    where
        R: DeserializeOwned,
    {
        let path = build_path([RESOURCE, identifier.as_ref()]);
        client.get(&path).await
    }

    /// Create a new blueprint definition.
    pub async fn create<B, R>(client: &PortClient, payload: &B) -> Result<R, PortError>
    where
        B: Serialize + ?Sized,
        R: DeserializeOwned,
    {
        let path = build_path([RESOURCE]);
        client.post(&path, payload).await
    }

    /// Replace the blueprint definition entirely.
    pub async fn update<B, R>(
        client: &PortClient,
        identifier: impl AsRef<str>,
        payload: &B,
    ) -> Result<R, PortError>
    where
        B: Serialize + ?Sized,
        R: DeserializeOwned,
    {
        let path = build_path([RESOURCE, identifier.as_ref()]);
        client.put(&path, payload).await
    }

    /// Apply a partial update to a blueprint.
    pub async fn patch<B, R>(
        client: &PortClient,
        identifier: impl AsRef<str>,
        payload: &B,
    ) -> Result<R, PortError>
    where
        B: Serialize + ?Sized,
        R: DeserializeOwned,
    {
        let path = build_path([RESOURCE, identifier.as_ref()]);
        client.patch(&path, payload).await
    }

    /// Delete a blueprint.
    pub async fn delete<R>(client: &PortClient, identifier: impl AsRef<str>) -> Result<R, PortError>
    where
        R: DeserializeOwned,
    {
        let path = build_path([RESOURCE, identifier.as_ref()]);
        client.delete(&path).await
    }

    /// Create a blueprint and immediately publish it by clearing the draft flag.
    pub async fn create_and_publish<R>(
        client: &PortClient,
        payload: &BlueprintDefinition,
    ) -> Result<R, PortError>
    where
        R: DeserializeOwned,
    {
        let created = create::<_, BlueprintDefinition>(client, payload).await?;
        // In the real API this would toggle the draft flag. Here we re-use the created definition.
        patch::<_, R>(client, &created.identifier, payload).await
    }
}

pub mod entities {
    use super::*;
    use crate::types::entities::{EntityRequest, EntityResponse};

    const RESOURCE: &str = "entities";

    fn entity_collection_path(blueprint_id: &str) -> String {
        build_path(["blueprints", blueprint_id, RESOURCE])
    }

    fn entity_item_path(blueprint_id: &str, entity_id: &str) -> String {
        build_path(["blueprints", blueprint_id, RESOURCE, entity_id])
    }

    /// List entities for a blueprint, applying query filters and pagination.
    pub async fn list<R, Q>(
        client: &PortClient,
        blueprint_id: impl AsRef<str>,
        query: Option<&Q>,
        pagination: Option<&Pagination>,
    ) -> Result<R, PortError>
    where
        R: DeserializeOwned,
        Q: Serialize + ?Sized,
    {
        let blueprint_id = blueprint_id.as_ref();
        let path = entity_collection_path(blueprint_id);
        list_helper(client, &path, query, pagination).await
    }

    /// Convenience wrapper for listing entities without filters.
    pub async fn list_all<R>(
        client: &PortClient,
        blueprint_id: impl AsRef<str>,
        pagination: Option<&Pagination>,
    ) -> Result<R, PortError>
    where
        R: DeserializeOwned,
    {
        list(client, blueprint_id, None::<&EmptyQuery>, pagination).await
    }

    pub async fn get<R>(
        client: &PortClient,
        blueprint_id: impl AsRef<str>,
        entity_id: impl AsRef<str>,
    ) -> Result<R, PortError>
    where
        R: DeserializeOwned,
    {
        let path = entity_item_path(blueprint_id.as_ref(), entity_id.as_ref());
        client.get(&path).await
    }

    /// Create a new entity instance and register it with the resource tracker.
    pub async fn create(
        client: &PortClient,
        blueprint_id: impl AsRef<str>,
        payload: &EntityRequest,
    ) -> Result<EntityResponse, PortError> {
        let blueprint_id = blueprint_id.as_ref();
        let path = entity_collection_path(blueprint_id);
        let response: EntityResponse = client.post(&path, payload).await?;
        let composite_id = format!("{}/{}", blueprint_id, response.identifier);
        client.record_creation(payload.resource_type(), &composite_id);
        Ok(response)
    }

    /// Upsert an entity via PUT.
    pub async fn upsert(
        client: &PortClient,
        blueprint_id: impl AsRef<str>,
        entity_id: impl AsRef<str>,
        payload: &EntityRequest,
    ) -> Result<EntityResponse, PortError> {
        let blueprint_id = blueprint_id.as_ref();
        let entity_id = entity_id.as_ref();
        let path = entity_item_path(blueprint_id, entity_id);
        let response: EntityResponse = client.put(&path, payload).await?;
        let composite_id = format!("{}/{}", blueprint_id, response.identifier);
        client.record_creation(payload.resource_type(), &composite_id);
        Ok(response)
    }

    pub async fn patch<B, R>(
        client: &PortClient,
        blueprint_id: impl AsRef<str>,
        entity_id: impl AsRef<str>,
        payload: &B,
    ) -> Result<R, PortError>
    where
        B: Serialize,
        R: DeserializeOwned,
    {
        let path = entity_item_path(blueprint_id.as_ref(), entity_id.as_ref());
        client.patch(&path, payload).await
    }

    pub async fn delete<R>(
        client: &PortClient,
        blueprint_id: impl AsRef<str>,
        entity_id: impl AsRef<str>,
    ) -> Result<R, PortError>
    where
        R: DeserializeOwned,
    {
        let blueprint_id = blueprint_id.as_ref();
        let entity_id = entity_id.as_ref();
        let path = entity_item_path(blueprint_id, entity_id);
        let response = client.delete(&path).await?;
        let composite_id = format!("{}/{}", blueprint_id, entity_id);
        client.record_deletion("entity", &composite_id);
        Ok(response)
    }

    /// Build an entity listing query using the fluent filter builder.
    pub fn filters() -> FilterBuilder {
        FilterBuilder::new()
    }
}

/// Scorecard endpoints.
pub mod scorecards {
    use super::*;
    use crate::types::scorecards::ScorecardRef;

    const RESOURCE: &str = "scorecards";

    /// List scorecards.
    pub async fn list(client: &PortClient) -> Result<Vec<ScorecardRef>, PortError> {
        let path = build_path([RESOURCE]);
        client.get(&path).await
    }
}

/// Automation runs endpoints.
pub mod runs {
    use super::*;
    use crate::types::runs::AutomationRun;

    const RESOURCE: &str = "runs";

    pub async fn get(
        client: &PortClient,
        run_id: impl AsRef<str>,
    ) -> Result<AutomationRun, PortError> {
        let path = build_path([RESOURCE, run_id.as_ref()]);
        client.get(&path).await
    }
}

/// High-level workflows that span multiple resources.
pub mod workflows {
    use super::*;
    use crate::types::blueprints::BlueprintDefinition;
    use crate::types::entities::EntityRequest;

    /// Bootstrap a blueprint and seed it with an initial entity.
    pub async fn bootstrap_blueprint(
        client: &PortClient,
        blueprint: &BlueprintDefinition,
        entity: &EntityRequest,
    ) -> Result<(), PortError> {
        super::blueprints::create::<_, BlueprintDefinition>(client, blueprint).await?;
        super::entities::create(client, &entity.blueprint, entity).await?;
        Ok(())
    }
}