Skip to content

Commit

Permalink
chore: extend API doc comments for Rust SDK
Browse files Browse the repository at this point in the history
  • Loading branch information
j-lanson authored and alilleybrinker committed Sep 23, 2024
1 parent 9b0bb29 commit e7d1a86
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 36 deletions.
10 changes: 5 additions & 5 deletions plugins/dummy_rand_data_sdk/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ impl Query for RandDataPlugin {
input: Value,
) -> hipcheck_sdk::error::Result<Value> {
let Value::Number(num_size) = input else {
return Err(Error::UnexpectedPluginQueryDataFormat);
return Err(Error::UnexpectedPluginQueryInputFormat);
};

let Some(size) = num_size.as_u64() else {
return Err(Error::UnexpectedPluginQueryDataFormat);
return Err(Error::UnexpectedPluginQueryInputFormat);
};

let reduced_num = reduce(size);
Expand All @@ -53,17 +53,17 @@ impl Query for RandDataPlugin {
.await?;

let Value::Array(mut sha256) = value else {
return Err(Error::UnexpectedPluginQueryDataFormat);
return Err(Error::UnexpectedPluginQueryInputFormat);
};

let Value::Number(num) = sha256.pop().unwrap() else {
return Err(Error::UnexpectedPluginQueryDataFormat);
return Err(Error::UnexpectedPluginQueryInputFormat);
};

match num.as_u64() {
Some(val) => return Ok(Value::Number(val.into())),
None => {
return Err(Error::UnexpectedPluginQueryDataFormat);
return Err(Error::UnexpectedPluginQueryInputFormat);
}
}
}
Expand Down
22 changes: 17 additions & 5 deletions sdk/rust/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ use std::{convert::Infallible, ops::Not, result::Result as StdResult};
use tokio::sync::mpsc::error::SendError as TokioMpscSendError;
use tonic::Status as TonicStatus;

/// An enumeration of errors that can occur in a Hipcheck plugin
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// An unknown error occurred, the query is in an unspecified state
#[error("unknown error; query is in an unspecified state")]
UnspecifiedQueryState,

/// The `PluginEngine` received a message with the unexpected status `ReplyInProgress`
#[error("unexpected ReplyInProgress state for query")]
UnexpectedReplyInProgress,

Expand All @@ -27,26 +30,31 @@ pub enum Error {
#[source] TokioMpscSendError<StdResult<InitiateQueryProtocolResponse, TonicStatus>>,
),

/// The `PluginEngine` received a message with a reply-type status when it expected a request
#[error("plugin sent QueryReply when server was expecting a request")]
ReceivedReplyWhenExpectingRequest,

/// The `PluginEngine` received a message with a request-type status when it expected a reply
#[error("plugin sent QuerySubmit when server was expecting a reply chunk")]
ReceivedSubmitWhenExpectingReplyChunk,

/// The `PluginEngine` received additional messages when it did not expect any
#[error("received additional message for ID '{id}' after query completion")]
MoreAfterQueryComplete { id: usize },

#[error("failed to start server")]
FailedToStartServer(#[source] tonic::transport::Error),

/// The `Query::run` function implementation received an incorrectly-typed JSON Value key
#[error("unexpected JSON value from plugin")]
UnexpectedPluginQueryDataFormat,
UnexpectedPluginQueryInputFormat,

/// The `PluginEngine` received a request for an unknown query endpoint
#[error("could not determine which plugin query to run")]
UnknownPluginQuery,

#[error("invalid format for QueryTarget")]
InvalidQueryTarget,
InvalidQueryTargetFormat,
}

// this will never happen, but is needed to enable passing QueryTarget to PluginEngine::query
Expand All @@ -58,29 +66,33 @@ impl From<Infallible> for Error {

pub type Result<T> = StdResult<T, Error>;

/// Errors specific to the execution of `Plugin::set_configuration()` to configure a Hipcheck
/// plugin.
#[derive(Debug)]
pub enum ConfigError {
/// The config key was valid, but the associated value was invalid
InvalidConfigValue {
field_name: String,
value: String,
reason: String,
},

/// The config was missing an expected field
MissingRequiredConfig {
field_name: String,
field_type: String,
possible_values: Vec<String>,
},

/// The config included an unrecognized field
UnrecognizedConfig {
field_name: String,
field_value: String,
possible_confusables: Vec<String>,
},

Unspecified {
message: String,
},
/// An unspecified error
Unspecified { message: String },
}

impl From<ConfigError> for SetConfigurationResponse {
Expand Down
64 changes: 45 additions & 19 deletions sdk/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ pub mod error;
pub mod plugin_engine;
pub mod plugin_server;

// utility module, so users can write `use hipcheck_sdk::prelude::*` and have everything they need to write a plugin
/// A utility module, users can simply write `use hipcheck_sdk::prelude::*` to import everything
/// they need to write a plugin
pub mod prelude {
pub use crate::deps::*;
pub use crate::error::{ConfigError, Error, Result};
Expand All @@ -26,13 +27,18 @@ pub mod prelude {
pub use crate::{DynQuery, NamedQuery, Plugin, Query, QuerySchema, QueryTarget};
}

// re-export of user facing third party dependencies
/// re-export of user-facing third-party dependencies
pub mod deps {
pub use schemars::schema::SchemaObject as JsonSchema;
pub use serde_json::{from_str, Value};
pub use tonic::async_trait;
}

/// The target of a Hipcheck query. The `publisher` and `plugin` fields are necessary to identify a
/// plugin process. Plugins may define one or more query endpoints, and may include an unnamed
/// endpoint as the "default", hence why the `query` field is of type Option. QueryTarget
/// implements `FromStr`, taking strings of the format `"publisher/plugin[/query]"` where the
/// bracketed substring is optional.
#[derive(Debug, Clone)]
pub struct QueryTarget {
pub publisher: String,
Expand All @@ -56,7 +62,7 @@ impl FromStr for QueryTarget {
plugin: plugin.to_string(),
query: None,
}),
_ => Err(Error::InvalidQueryTarget),
_ => Err(Error::InvalidQueryTargetFormat),
}
}
}
Expand All @@ -68,74 +74,94 @@ impl TryInto<QueryTarget> for &str {
}
}

/// Encapsulates the signature of a particular `NamedQuery`. Instances of this type are usually
/// created by the default implementation of `Plugin::schemas()` and would not need to be created
/// by hand unless you are doing something very unorthodox.
pub struct QuerySchema {
/// The name of the query being described.
query_name: &'static str,

/// The query's input schema.
/// The query's input schema as a `schemars::schema::SchemaObject`.
input_schema: JsonSchema,

/// The query's output schema.
/// The query's output schema as a `schemars::schema::SchemaObject`.
output_schema: JsonSchema,
}

/// Query trait object.
/// A `Query` trait object.
pub type DynQuery = Box<dyn Query>;

/// Since the `Query` trait needs to be made into a trait object, we can't use a static associated
/// string to store the query's name in the trait itself. This object wraps a `Query` trait object
/// and allows us to associate a name with it.
pub struct NamedQuery {
/// The name of the query.
pub name: &'static str,

/// The query object.
/// The `Query` trait object.
pub inner: DynQuery,
}

impl NamedQuery {
/// Is the current query the default query?
/// Returns whether the current query is the plugin's default query, determined by whether the
/// query name is empty.
fn is_default(&self) -> bool {
self.name.is_empty()
}
}

/// Defines a single query for the plugin.
/// Defines a single query endpoint for the plugin.
#[tonic::async_trait]
pub trait Query: Send {
/// Get the input schema for the query.
/// Get the input schema for the query as a `schemars::schema::SchemaObject`.
fn input_schema(&self) -> JsonSchema;

/// Get the output schema for the query.
/// Get the output schema for the query as a `schemars::schema::SchemaObject`.
fn output_schema(&self) -> JsonSchema;

/// Run the plugin, optionally making queries to other plugins.
/// Run the query endpoint logic on `input`, returning a JSONified return value on success.
/// The `PluginEngine` reference allows the endpoint to query other Hipcheck plugins by
/// calling `engine::query()`.
async fn run(&self, engine: &mut PluginEngine, input: JsonValue) -> Result<JsonValue>;
}

/// The core trait that a plugin author must implement to write a plugin with the Hipcheck SDK.
/// Declares basic information about the plugin and its query endpoints, and accepts a
/// configuration map from Hipcheck core.
pub trait Plugin: Send + Sync + 'static {
/// The name of the publisher of the pl∂ugin.
/// The name of the plugin publisher.
const PUBLISHER: &'static str;

/// The name of the plugin.
const NAME: &'static str;

/// Handles setting configuration.
/// Handle setting configuration. The `config` parameter is a JSON object of `String, String`
/// pairs.
fn set_config(&self, config: JsonValue) -> StdResult<(), ConfigError>;

/// Gets the plugin's default policy expression.
/// Get the plugin's default policy expression. This will only ever be called after
/// `Plugin::set_config()`. For more information on policy expression syntax, see the Hipcheck
/// website.
fn default_policy_expr(&self) -> Result<String>;

/// Gets a description of what is returned by the plugin's default query.
/// Get an unstructured description of what is returned by the plugin's default query.
fn explain_default_query(&self) -> Result<Option<String>>;

/// Get all the queries supported by the plugin.
/// Get all the queries supported by the plugin. Each query endpoint in a plugin will have its
/// own `trait Query` implementation. This function should return an iterator containing one
/// `NamedQuery` instance ofr each `trait Query` implementation defined by the plugin author.
fn queries(&self) -> impl Iterator<Item = NamedQuery>;

/// Get the plugin's default query, if it has one.
/// Get the plugin's default query, if it has one. The default query is a `NamedQuery` with an
/// empty `name` string. In most cases users should not need to override the default
/// implementation.
fn default_query(&self) -> Option<DynQuery> {
self.queries()
.find_map(|named| named.is_default().then_some(named.inner))
}

/// Get all schemas for queries provided by the plugin.
/// Get all schemas for queries provided by the plugin. In most cases users should not need to
/// override the default implementation.
fn schemas(&self) -> impl Iterator<Item = QuerySchema> {
self.queries().map(|query| QuerySchema {
query_name: query.name,
Expand Down
9 changes: 8 additions & 1 deletion sdk/rust/src/plugin_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ impl TryFrom<PluginQuery> for Query {

type SessionTracker = HashMap<i32, mpsc::Sender<Option<PluginQuery>>>;

/// The handle that a `Query::run()` function can use to request information from other Hipcheck
/// plugins in order to fulfill a query.
pub struct PluginEngine {
id: usize,
tx: mpsc::Sender<StdResult<InitiateQueryProtocolResponse, Status>>,
Expand All @@ -96,6 +98,11 @@ pub struct PluginEngine {
}

impl PluginEngine {
/// Query another Hipcheck plugin `target` with key `input`. On success, the JSONified result
/// of the query is returned. `target` will often be a string of the format
/// `"publisher/plugin[/query]"`, where the bracketed substring is optional if the plugin's
/// default query endpoint is desired. `input` must of a type implementing `Into<JsonValue>`,
/// which can be done by deriving or implementing `serde::Serialize`.
pub async fn query<T, V>(&mut self, target: T, input: V) -> Result<JsonValue>
where
T: TryInto<QueryTarget, Error: Into<Error>>,
Expand Down Expand Up @@ -184,7 +191,7 @@ impl PluginEngine {
Ok(Some(out))
}

/// Send a gRPC query from plugin to the hipcheck server
// Send a gRPC query from plugin to the hipcheck server
async fn send(&self, query: Query) -> Result<()> {
let query = InitiateQueryProtocolResponse {
query: Some(self.convert(query)?),
Expand Down
9 changes: 3 additions & 6 deletions sdk/rust/src/plugin_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,9 @@ use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream as RecvStream;
use tonic::{transport::Server, Code, Request as Req, Response as Resp, Status, Streaming};

/// Runs the Hipcheck plugin protocol based on the user's plugin definition.
/// Runs the Hipcheck plugin protocol based on the user's implementation of the `Plugin` trait.
///
/// The key idea is that this implements the gRPC mechanics and handles all
/// the details of the query protocol, so that the user doesn't need to do
/// anything more than define queries as asynchronous functions with associated
/// input and output schemas.
/// This struct implements the underlying gRPC protocol that is not exposed to the plugin author.
pub struct PluginServer<P> {
plugin: Arc<P>,
}
Expand Down Expand Up @@ -56,7 +53,7 @@ impl<P: Plugin> PluginServer<P> {
}
}

/// The result of running a query.
/// The result of running a query, where the error is of the type `tonic::Status`.
pub type QueryResult<T> = StdResult<T, Status>;

#[tonic::async_trait]
Expand Down

0 comments on commit e7d1a86

Please sign in to comment.