reasoninglayer 1.0.3

Rust client SDK for the Reasoning Layer API
Documentation
//! Sort (type hierarchy) operations.
//!
//! This is the **pilot** module for the OpenAPI-driven DTO migration: every
//! sort-related type is sourced from [`crate::api_spec`] (auto-generated from
//! `api-spec/openapi.json`) instead of the hand-written `crate::types::sorts`.

use std::sync::Arc;

use uuid::Uuid;

use crate::api_spec::{
    ApproveLearnedSimilarityRequest, ApproveLearnedSimilarityResponse, BulkCreateSortsRequest,
    BulkCreateSortsResponse, BulkSetSimilaritiesRequest, BulkSetSimilaritiesResponse,
    ComputeGlbRequest, ComputeGlbResponse, ComputeLubRequest, ComputeLubResponse,
    CreateSortRequest, DecodeGlbResponse, GetEquivalenceClassesResponse, GetPreorderDegreeRequest,
    GetPreorderDegreeResponse, GetSortSimilarityRequest, GetSortSimilarityResponse,
    IsSubtypeResponse, LearnSortSimilaritiesRequest, LearnSortSimilaritiesResponse,
    LearnedSimilarityListResponse, RejectLearnedSimilarityRequest, RejectLearnedSimilarityResponse,
    SetSortSimilarityRequest, SetSortSimilarityResponse, SortAncestorsResponse,
    SortChildrenResponse, SortCompareOperator, SortCompareRequest, SortCompareResponse,
    SortDescendantsResponse, SortDto, SortInfoDto, SortListResponse, SortParentsResponse,
    SortResponse, UpdateSortReviewRequest,
};
use crate::error::Error;
use crate::http::HttpClient;
use crate::types::common::RequestOptions;

/// Resource client for sort operations. Returned by [`ReasoningLayerClient::sorts`](crate::ReasoningLayerClient::sorts).
#[derive(Debug, Clone)]
pub struct SortsClient {
    http: HttpClient,
    tenant_id: Arc<String>,
}

impl SortsClient {
    pub(crate) fn new(http: HttpClient) -> Self {
        let tenant_id = Arc::new(http.config.tenant_id.clone());
        Self { http, tenant_id }
    }

    pub(crate) fn http(&self) -> &HttpClient {
        &self.http
    }

    /// Create a new sort.
    pub async fn create_sort(
        &self,
        request: CreateSortRequest,
        options: Option<&RequestOptions>,
    ) -> Result<SortDto, Error> {
        let response: SortResponse = self.http.post("/sorts", &request, options).await?;
        Ok(response.sort)
    }

    /// Get a sort by ID.
    pub async fn get_sort(
        &self,
        sort_id: &str,
        options: Option<&RequestOptions>,
    ) -> Result<SortDto, Error> {
        let path = format!("/sorts/{}", encode(sort_id));
        let response: SortResponse = self.http.get(&path, None, options).await?;
        Ok(response.sort)
    }

    /// Delete a sort by ID.
    pub async fn delete_sort(
        &self,
        sort_id: &str,
        options: Option<&RequestOptions>,
    ) -> Result<(), Error> {
        let path = format!("/sorts/{}", encode(sort_id));
        let _: serde_json::Value = self.http.delete(&path, None, options).await?;
        Ok(())
    }

    /// List all sorts for the authenticated tenant.
    pub async fn list_sorts(
        &self,
        options: Option<&RequestOptions>,
    ) -> Result<Vec<SortDto>, Error> {
        let path = format!("/sorts/tenant/{}", encode(self.tenant_id.as_str()));
        let response: SortListResponse = self.http.get(&path, None, options).await?;
        Ok(response.sorts)
    }

    /// Bulk-create sorts with name-based parent references.
    pub async fn bulk_create_sorts(
        &self,
        request: BulkCreateSortsRequest,
        options: Option<&RequestOptions>,
    ) -> Result<BulkCreateSortsResponse, Error> {
        self.http.post("/sorts/bulk", &request, options).await
    }

    /// Check if a sort is a subtype of another.
    pub async fn is_subtype(
        &self,
        child_id: &str,
        parent_id: &str,
        options: Option<&RequestOptions>,
    ) -> Result<bool, Error> {
        let path = format!("/sorts/{}/subtype/{}", encode(child_id), encode(parent_id));
        let response: IsSubtypeResponse = self.http.get(&path, None, options).await?;
        Ok(response.is_subtype)
    }

    /// Compute the Greatest Lower Bound (GLB) of two sorts.
    pub async fn compute_glb(
        &self,
        request: ComputeGlbRequest,
        options: Option<&RequestOptions>,
    ) -> Result<ComputeGlbResponse, Error> {
        self.http.post("/sorts/glb", &request, options).await
    }

    /// Compute the Least Upper Bound (LUB) of two sorts.
    pub async fn compute_lub(
        &self,
        request: ComputeLubRequest,
        options: Option<&RequestOptions>,
    ) -> Result<ComputeLubResponse, Error> {
        self.http.post("/sorts/lub", &request, options).await
    }

    /// Decode a GLB as a type disjunction.
    pub async fn decode_glb(
        &self,
        request: ComputeGlbRequest,
        options: Option<&RequestOptions>,
    ) -> Result<DecodeGlbResponse, Error> {
        self.http.post("/sorts/glb/decode", &request, options).await
    }

    /// Find the most specific type shared by two sorts — alias for [`SortsClient::compute_glb`].
    pub async fn find_common_subtype(
        &self,
        sort1_id: &str,
        sort2_id: &str,
        options: Option<&RequestOptions>,
    ) -> Result<ComputeGlbResponse, Error> {
        self.compute_glb(
            ComputeGlbRequest {
                sort1_id: parse_uuid(sort1_id, "sort1_id")?,
                sort2_id: parse_uuid(sort2_id, "sort2_id")?,
            },
            options,
        )
        .await
    }

    /// Find the most general type that covers both sorts — alias for [`SortsClient::compute_lub`].
    pub async fn find_common_supertype(
        &self,
        sort1_id: &str,
        sort2_id: &str,
        options: Option<&RequestOptions>,
    ) -> Result<ComputeLubResponse, Error> {
        self.compute_lub(
            ComputeLubRequest {
                sort1_id: parse_uuid(sort1_id, "sort1_id")?,
                sort2_id: parse_uuid(sort2_id, "sort2_id")?,
            },
            options,
        )
        .await
    }

    /// Explain the relationship between two sorts — alias for [`SortsClient::decode_glb`].
    pub async fn explain_common_subtype(
        &self,
        sort1_id: &str,
        sort2_id: &str,
        options: Option<&RequestOptions>,
    ) -> Result<DecodeGlbResponse, Error> {
        self.decode_glb(
            ComputeGlbRequest {
                sort1_id: parse_uuid(sort1_id, "sort1_id")?,
                sort2_id: parse_uuid(sort2_id, "sort2_id")?,
            },
            options,
        )
        .await
    }

    /// Get direct children of a sort.
    pub async fn get_children(
        &self,
        sort_id: &str,
        options: Option<&RequestOptions>,
    ) -> Result<Vec<SortInfoDto>, Error> {
        let path = format!("/sorts/{}/children", encode(sort_id));
        let response: SortChildrenResponse = self.http.get(&path, None, options).await?;
        Ok(response.children)
    }

    /// Get direct parents of a sort.
    pub async fn get_parents(
        &self,
        sort_id: &str,
        options: Option<&RequestOptions>,
    ) -> Result<Vec<SortInfoDto>, Error> {
        let path = format!("/sorts/{}/parents", encode(sort_id));
        let response: SortParentsResponse = self.http.get(&path, None, options).await?;
        Ok(response.parents)
    }

    /// Get all ancestors of a sort (transitive parents).
    pub async fn get_ancestors(
        &self,
        sort_id: &str,
        options: Option<&RequestOptions>,
    ) -> Result<Vec<SortInfoDto>, Error> {
        let path = format!("/sorts/{}/ancestors", encode(sort_id));
        let response: SortAncestorsResponse = self.http.get(&path, None, options).await?;
        Ok(response.ancestors)
    }

    /// Get all descendants of a sort (transitive children).
    pub async fn get_descendants(
        &self,
        sort_id: &str,
        options: Option<&RequestOptions>,
    ) -> Result<Vec<SortInfoDto>, Error> {
        let path = format!("/sorts/{}/descendants", encode(sort_id));
        let response: SortDescendantsResponse = self.http.get(&path, None, options).await?;
        Ok(response.descendants)
    }

    /// Get sorts compatible with a given sort.
    pub async fn get_compatible(
        &self,
        sort_id: &str,
        options: Option<&RequestOptions>,
    ) -> Result<Vec<SortDto>, Error> {
        let path = format!("/sorts/{}/compatible", encode(sort_id));
        let response: SortListResponse = self.http.get(&path, None, options).await?;
        Ok(response.sorts)
    }

    /// Compare two sorts using an operator. The `tenant_id` is injected from the client config.
    pub async fn compare_sorts(
        &self,
        operator: SortCompareOperator,
        sort_a: impl Into<String>,
        sort_b: impl Into<String>,
        options: Option<&RequestOptions>,
    ) -> Result<SortCompareResponse, Error> {
        let request = SortCompareRequest {
            operator,
            sort_a: sort_a.into(),
            sort_b: sort_b.into(),
            tenant_id: parse_uuid(self.tenant_id.as_str(), "tenant_id")?,
        };
        self.http.post("/sorts/compare", &request, options).await
    }

    /// Trigger re-indexing of the sort hierarchy.
    pub async fn index_sorts(&self, options: Option<&RequestOptions>) -> Result<(), Error> {
        let _: serde_json::Value = self
            .http
            .post("/sorts/index", &serde_json::json!({}), options)
            .await?;
        Ok(())
    }

    /// Update the review status of a sort.
    pub async fn update_review_status(
        &self,
        sort_id: &str,
        request: UpdateSortReviewRequest,
        options: Option<&RequestOptions>,
    ) -> Result<(), Error> {
        let path = format!("/sorts/{}/review", encode(sort_id));
        let _: serde_json::Value = self.http.post(&path, &request, options).await?;
        Ok(())
    }

    // ─── Similarity ───────────────────────────────────────────────────────

    /// Get the direct similarity degree between two sorts.
    pub async fn get_sort_similarity(
        &self,
        request: GetSortSimilarityRequest,
        options: Option<&RequestOptions>,
    ) -> Result<GetSortSimilarityResponse, Error> {
        self.http
            .post("/sorts/similarity/get", &request, options)
            .await
    }

    /// Set similarity between two sorts (symmetric).
    pub async fn set_sort_similarity(
        &self,
        request: SetSortSimilarityRequest,
        options: Option<&RequestOptions>,
    ) -> Result<SetSortSimilarityResponse, Error> {
        self.http.post("/sorts/similarity", &request, options).await
    }

    /// Bulk-set sort similarities.
    pub async fn bulk_set_similarities(
        &self,
        request: BulkSetSimilaritiesRequest,
        options: Option<&RequestOptions>,
    ) -> Result<BulkSetSimilaritiesResponse, Error> {
        self.http
            .post("/sorts/similarity/bulk", &request, options)
            .await
    }

    /// Compute the preorder degree between two sorts.
    pub async fn get_preorder_degree(
        &self,
        request: GetPreorderDegreeRequest,
        options: Option<&RequestOptions>,
    ) -> Result<GetPreorderDegreeResponse, Error> {
        self.http
            .post("/sorts/preorder-degree", &request, options)
            .await
    }

    /// Get equivalence classes under the combined preorder.
    pub async fn get_equivalence_classes(
        &self,
        options: Option<&RequestOptions>,
    ) -> Result<GetEquivalenceClassesResponse, Error> {
        self.http
            .get("/sorts/equivalence-classes", None, options)
            .await
    }

    /// Learn sort similarities from effect features.
    pub async fn learn_similarities(
        &self,
        request: LearnSortSimilaritiesRequest,
        options: Option<&RequestOptions>,
    ) -> Result<LearnSortSimilaritiesResponse, Error> {
        self.http
            .post("/sorts/learn-similarities", &request, options)
            .await
    }

    /// Get all learned similarities.
    pub async fn get_learned_similarities(
        &self,
        options: Option<&RequestOptions>,
    ) -> Result<LearnedSimilarityListResponse, Error> {
        self.http
            .get("/sorts/learned-similarities", None, options)
            .await
    }

    /// Approve a learned similarity.
    pub async fn approve_similarity(
        &self,
        request: ApproveLearnedSimilarityRequest,
        options: Option<&RequestOptions>,
    ) -> Result<ApproveLearnedSimilarityResponse, Error> {
        self.http
            .post("/sorts/learned-similarities/approve", &request, options)
            .await
    }

    /// Reject a learned similarity.
    pub async fn reject_similarity(
        &self,
        request: RejectLearnedSimilarityRequest,
        options: Option<&RequestOptions>,
    ) -> Result<RejectLearnedSimilarityResponse, Error> {
        self.http
            .post("/sorts/learned-similarities/reject", &request, options)
            .await
    }

    /// Alias for [`SortsClient::bulk_create_sorts`].
    pub async fn create_many(
        &self,
        request: BulkCreateSortsRequest,
        options: Option<&RequestOptions>,
    ) -> Result<BulkCreateSortsResponse, Error> {
        self.bulk_create_sorts(request, options).await
    }
}

fn encode(segment: &str) -> String {
    // Only percent-encode characters that are unsafe in a path segment. UUIDs and names with
    // hyphens/underscores pass through unchanged; anything else is encoded defensively.
    url::form_urlencoded::byte_serialize(segment.as_bytes()).collect()
}

fn parse_uuid(value: &str, field: &'static str) -> Result<Uuid, Error> {
    Uuid::parse_str(value).map_err(|err| Error::Validation {
        field: Some(field.to_string()),
        message: format!("expected a UUID, got {value:?}: {err}"),
    })
}