Skip to content

Commit

Permalink
Add StaticFilter trait remove factories
Browse files Browse the repository at this point in the history
  • Loading branch information
XAMPPRocky committed Apr 13, 2022
1 parent 3952a71 commit 3f7680f
Show file tree
Hide file tree
Showing 35 changed files with 620 additions and 659 deletions.
134 changes: 84 additions & 50 deletions docs/src/filters/writing_custom_filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ A [trait][Filter] representing an actual [Filter][built-in-filters] instance in
A [trait][FilterFactory] representing a type that knows how to create instances of a particular type of [Filter].

- An implementation provides a `name` and `create_filter` method.
- `create_filter` takes in [configuration][filter configuration] for the filter to create and returns a [FilterInstance] type containing a new instance of its filter type.
- `create_filter` takes in [configuration][filter configuration] for the filter to create and returns a [FilterInstance] type containing a new instance of its filter type.
`name` returns the Filter name - a unique identifier of filters of the created type (e.g quilkin.filters.debug.v1alpha1.Debug).

### FilterRegistry
Expand Down Expand Up @@ -72,7 +72,7 @@ We start with the [Filter] implementation
#
// src/main.rs
use quilkin::filters::prelude::*;
struct Greet;
impl Filter for Greet {
Expand All @@ -94,31 +94,70 @@ Next, we implement a [FilterFactory] for it and give it a name:
# #![allow(unused)]
# fn main() {
#
# #[derive(Default)]
# struct Greet;
# impl Greet {
# fn new(_: Config) -> Self {
# <_>::default()
# }
# }
# impl Filter for Greet {}
# use quilkin::filters::Filter;
# impl StaticFilter for Greet {
# const NAME: &'static str = "greet.v1";
# type Configuration = Config;
# type BinaryConfiguration = prost_types::Struct;
#
# fn new(config: Option<Self::Configuration>) -> Result<Self, Error> {
# Ok(Greet::new(config.unwrap_or_default()))
# }
# }
// src/main.rs
use quilkin::filters::prelude::*;
pub const NAME: &str = "greet.v1";
pub fn factory() -> DynFilterFactory {
Box::from(GreetFilterFactory)
#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
struct Config {
greeting: String,
}
struct GreetFilterFactory;
impl FilterFactory for GreetFilterFactory {
fn name(&self) -> &'static str {
NAME
impl Default for Config {
fn default() -> Self {
Self {
greeting: "World".into(),
}
}
}
fn config_schema(&self) -> schemars::schema::RootSchema {
schemars::schema_for!(serde_json::Value)
impl TryFrom<prost_types::Struct> for Config {
type Error = Error;
fn try_from(map: prost_types::Struct) -> Result<Self, Error> {
let greeting = map.fields.get("greeting")
.and_then(|v| v.kind.clone())
.and_then(|kind| {
match kind {
prost_types::value::Kind::StringValue(string) => Some(string),
_ => None,
}
}).ok_or_else(|| {
Error::FieldInvalid {
field: "greeting".into(),
reason: "Missing".into()
}
})?;
Ok(Self { greeting })
}
}
fn create_filter(&self, _: CreateFilterArgs) -> Result<FilterInstance, Error> {
let filter: Box<dyn Filter> = Box::new(Greet);
Ok(FilterInstance::new(serde_json::Value::Null, filter))
impl From<Config> for prost_types::Struct {
fn from(config: Config) -> Self {
Self {
fields: <_>::from([
("greeting".into(), prost_types::Value {
kind: Some(prost_types::value::Kind::StringValue(config.greeting))
})
])
}
}
}
# }
Expand All @@ -130,7 +169,7 @@ impl FilterFactory for GreetFilterFactory {
#### 3. Start the proxy

We can run the proxy in the exact manner as the default Quilkin binary using the [run][runner::run] function, passing in our custom [FilterFactory].
Let's add a main function that does that. Quilkin relies on the [Tokio] async runtime, so we need to import that
Let's add a main function that does that. Quilkin relies on the [Tokio] async runtime, so we need to import that
crate and wrap our main function with it.

Add Tokio as a dependency in `Cargo.toml`.
Expand Down Expand Up @@ -220,41 +259,36 @@ First let's create the config for our static configuration:
```rust,no_run,noplayground
// src/main.rs
# use serde::{Deserialize, Serialize};
# #[derive(Serialize, Deserialize, Debug)]
# use quilkin::filters::prelude::*;
# #[derive(Serialize, Default, Deserialize, Debug, schemars::JsonSchema)]
# struct Config {
# greeting: String,
# }
# use quilkin::filters::prelude::*;
# #[derive(Default)]
# struct Greet(String);
# impl Greet {
# fn new(_: Config) -> Self { <_>::default() }
# }
# impl Filter for Greet { }
use quilkin::config::ConfigType;
pub const NAME: &str = "greet.v1";
pub fn factory() -> DynFilterFactory {
Box::from(GreetFilterFactory)
}
struct GreetFilterFactory;
impl FilterFactory for GreetFilterFactory {
fn name(&self) -> &'static str {
NAME
}
fn config_schema(&self) -> schemars::schema::RootSchema {
schemars::schema_for!(serde_json::Value)
}
fn create_filter(&self, args: CreateFilterArgs) -> Result<FilterInstance, Error> {
let config = match args.config.unwrap() {
ConfigType::Static(config) => {
serde_yaml::from_str::<Config>(serde_yaml::to_string(&config).unwrap().as_str())
.unwrap()
}
ConfigType::Dynamic(_) => unimplemented!("dynamic config is not yet supported for this filter"),
};
let filter: Box<dyn Filter> = Box::new(Greet(config.greeting));
Ok(FilterInstance::new(serde_json::Value::Null, filter))
# impl TryFrom<prost_types::Struct> for Config {
# type Error = Error;
# fn try_from(map: prost_types::Struct) -> Result<Self, Error> {
# todo!()
# }
# }
# impl TryFrom<Config> for prost_types::Struct {
# type Error = Error;
# fn try_from(map: Config) -> Result<Self, Error> {
# todo!()
# }
# }
impl StaticFilter for Greet {
# const NAME: &'static str = "greet.v1";
# type Configuration = Config;
# type BinaryConfiguration = prost_types::Struct;
#
fn new(config: Option<Self::Configuration>) -> Result<Self, Error> {
Ok(Greet::new(config.unwrap_or_default()))
}
}
```
Expand Down Expand Up @@ -282,7 +316,7 @@ let config = match args.config.unwrap() {

The [Dynamic][ConfigType::dynamic] contains the serialized [Protobuf] message received from the [management server] for the [Filter] to create.
As a result, its contents are entirely opaque to Quilkin and it is represented with the [Prost Any][prost-any] type so the [FilterFactory]
can interpret its contents however it wishes.
can interpret its contents however it wishes.
However, it usually contains a Protobuf equivalent of the filter's static configuration.

###### 1. Add the proto parsing crates to `Cargo.toml`:
Expand Down Expand Up @@ -334,9 +368,9 @@ recreating the grpc package name as Rust modules:
###### 4. Decode the serialized proto message into a config:

If the message contains a Protobuf equivalent of the filter's static configuration, we can
leverage the [deserialize][ConfigType::deserialize] method to deserialize either a static or dynamic config.
leverage the [deserialize][ConfigType::deserialize] method to deserialize either a static or dynamic config.
The function automatically deserializes and converts from the Protobuf type if the input contains a dynamic
configuration.
configuration.
As a result, the function requires that the [std::convert::TryFrom] is implemented from our dynamic
config type to a static equivalent.

Expand Down
20 changes: 11 additions & 9 deletions src/config/config_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use std::convert::TryFrom;

use bytes::Bytes;

use crate::filters::{ConvertProtoConfigError, Error};
use crate::filters::Error;

/// The configuration of a [`Filter`][crate::filters::Filter] from either a
/// static or dynamic source.
Expand Down Expand Up @@ -48,19 +48,21 @@ impl ConfigType {
/// It returns both the deserialized, as well as, a JSON representation
/// of the provided config.
/// It returns an error if any of the serialization or deserialization steps fail.
pub fn deserialize<Static, Dynamic>(
pub fn deserialize<TextConfiguration, BinaryConfiguration>(
self,
filter_name: &str,
) -> Result<(serde_json::Value, Static), Error>
) -> Result<(serde_json::Value, TextConfiguration), Error>
where
Dynamic: prost::Message + Default,
Static: serde::Serialize
+ for<'de> serde::Deserialize<'de>
+ TryFrom<Dynamic, Error = ConvertProtoConfigError>,
BinaryConfiguration: prost::Message + Default,
TextConfiguration:
serde::Serialize + for<'de> serde::Deserialize<'de> + TryFrom<BinaryConfiguration>,
Error: From<<BinaryConfiguration as TryInto<TextConfiguration>>::Error>,
{
match self {
ConfigType::Static(ref config) => serde_yaml::to_string(config)
.and_then(|raw_config| serde_yaml::from_str::<Static>(raw_config.as_str()))
.and_then(|raw_config| {
serde_yaml::from_str::<TextConfiguration>(raw_config.as_str())
})
.map_err(|err| {
Error::DeserializeFailed(format!(
"filter `{filter_name}`: failed to YAML deserialize config: {err}",
Expand All @@ -76,7 +78,7 @@ impl ConfigType {
"filter `{filter_name}`: config decode error: {err}",
))
})
.and_then(|config| Static::try_from(config).map_err(Error::ConvertProtoConfig))
.and_then(|config| TextConfiguration::try_from(config).map_err(From::from))
.and_then(|config| {
Self::get_json_config(filter_name, &config)
.map(|config_json| (config_json, config))
Expand Down
72 changes: 67 additions & 5 deletions src/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@

//! Filters for processing packets.
mod chain;
mod error;
mod factory;
mod metadata;
mod read;
mod registry;
mod set;
mod write;

pub(crate) mod chain;

pub mod capture;
pub mod compress;
pub mod concatenate_bytes;
Expand All @@ -34,7 +34,6 @@ pub mod firewall;
pub mod load_balancer;
pub mod local_rate_limit;
pub mod r#match;
pub mod metadata;
pub mod pass;
pub mod token_router;

Expand All @@ -43,21 +42,84 @@ pub mod token_router;
pub mod prelude {
pub use super::{
ConvertProtoConfigError, CreateFilterArgs, DynFilterFactory, Error, Filter, FilterFactory,
FilterInstance, ReadContext, ReadResponse, WriteContext, WriteResponse,
FilterInstance, ReadContext, ReadResponse, StaticFilter, WriteContext, WriteResponse,
};
}

// Core Filter types
pub use self::{
capture::Capture,
compress::Compress,
concatenate_bytes::ConcatenateBytes,
debug::Debug,
drop::Drop,
error::{ConvertProtoConfigError, Error},
factory::{CreateFilterArgs, DynFilterFactory, FilterFactory, FilterInstance},
firewall::Firewall,
load_balancer::LoadBalancer,
local_rate_limit::LocalRateLimit,
pass::Pass,
r#match::Match,
read::{ReadContext, ReadResponse},
registry::FilterRegistry,
set::{FilterMap, FilterSet},
token_router::TokenRouter,
write::{WriteContext, WriteResponse},
};

pub(crate) use self::chain::{FilterChain, SharedFilterChain};
pub(crate) use self::chain::{Error as FilterChainError, FilterChain, SharedFilterChain};

/// Statically safe version of [`Filter`], if you're writing a Rust filter, you
/// should implement [`StaticFilter`] in addition to [`Filter`], as
/// [`StaticFilter`] guarantees all of the required properties through the type
/// system, allowing Quilkin take care of the virtual table boilerplate
/// automatically at compile-time.
pub trait StaticFilter: Filter + Sized
// This where clause simply states that `Configuration`'s and
// `BinaryConfiguration`'s `Error` types are compatible with `filters::Error`.
where
Error: From<<Self::Configuration as TryFrom<Self::BinaryConfiguration>>::Error>
+ From<<Self::BinaryConfiguration as TryFrom<Self::Configuration>>::Error>,
{
/// The globally unique name of the filter.
const NAME: &'static str;
/// The human-readable configuration of the filter. **Must** be [`serde`]
/// compatible, have a JSON schema, and be convertible to and
/// from [`Self::BinaryConfiguration`].
type Configuration: schemars::JsonSchema
+ serde::Serialize
+ for<'de> serde::Deserialize<'de>
+ TryFrom<Self::BinaryConfiguration>;
/// The binary configuration of the filter. **Must** be [`prost`] compatible,
/// and be convertible to and from [`Self::Configuration`].
type BinaryConfiguration: prost::Message
+ Default
+ TryFrom<Self::Configuration>
+ Send
+ Sync
+ Sized;

/// Instaniates a new [`StaticFilter`] from the given configuration, if any.
/// # Errors
/// If the provided configuration is invalid.
fn new(config: Option<Self::Configuration>) -> Result<Self, Error>;

/// Creates a new dynamic [`FilterFactory`] virtual table.
fn factory() -> DynFilterFactory
where
Self: 'static,
{
Box::from(std::marker::PhantomData::<fn() -> Self>)
}

/// Convenience method for providing a consistent error message for filters
/// which require a fully initialized [`Self::Configuration`].
fn ensure_config_exists(
config: Option<Self::Configuration>,
) -> Result<Self::Configuration, Error> {
config.ok_or(Error::MissingConfig(Self::NAME))
}
}

/// Trait for routing and manipulating packets.
///
Expand Down
Loading

0 comments on commit 3f7680f

Please sign in to comment.