Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add StaticFilter trait remove factories
Browse files Browse the repository at this point in the history
XAMPPRocky committed Apr 13, 2022

Unverified

This user has not yet uploaded their public signing key.
1 parent 3952a71 commit 11f7649
Showing 36 changed files with 642 additions and 685 deletions.
134 changes: 84 additions & 50 deletions docs/src/filters/writing_custom_filters.md
Original file line number Diff line number Diff line change
@@ -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
@@ -72,7 +72,7 @@ We start with the [Filter] implementation
#
// src/main.rs
use quilkin::filters::prelude::*;
struct Greet;
impl Filter for Greet {
@@ -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))
})
])
}
}
}
# }
@@ -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`.
@@ -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()))
}
}
```
@@ -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`:
@@ -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.

48 changes: 22 additions & 26 deletions examples/quilkin-filter-example/src/main.rs
Original file line number Diff line number Diff line change
@@ -15,8 +15,9 @@
*/

// ANCHOR: include_proto
quilkin::include_proto!("greet");
use greet::Greet as ProtoGreet;
mod proto {
tonic::include_proto!("greet");
}
// ANCHOR_END: include_proto
use quilkin::filters::prelude::*;

@@ -31,15 +32,23 @@ struct Config {
// ANCHOR_END: serde_config

// ANCHOR: TryFrom
impl TryFrom<ProtoGreet> for Config {
impl TryFrom<proto::Greet> for Config {
type Error = ConvertProtoConfigError;

fn try_from(p: ProtoGreet) -> Result<Self, Self::Error> {
Ok(Config {
fn try_from(p: proto::Greet) -> Result<Self, Self::Error> {
Ok(Self {
greeting: p.greeting,
})
}
}

impl From<Config> for proto::Greet {
fn from(config: Config) -> Self {
Self {
greeting: config.greeting,
}
}
}
// ANCHOR_END: TryFrom

// ANCHOR: filter
@@ -60,28 +69,15 @@ impl Filter for Greet {
// ANCHOR_END: filter

// ANCHOR: factory
pub const NAME: &str = "greet.v1";
use quilkin::filters::StaticFilter;

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!(Config)
}
impl StaticFilter for Greet {
const NAME: &'static str = "greet.v1";
type Configuration = Config;
type BinaryConfiguration = proto::Greet;

fn create_filter(&self, args: CreateFilterArgs) -> Result<FilterInstance, Error> {
let (config_json, config) = self
.require_config(args.config)?
.deserialize::<Config, ProtoGreet>(self.name())?;
let filter: Box<dyn Filter> = Box::new(Greet(config.greeting));
Ok(FilterInstance::new(config_json, filter))
fn new(config: Option<Self::Configuration>) -> Result<Self, quilkin::filters::Error> {
Ok(Self(Self::ensure_config_exists(config)?.greeting))
}
}
// ANCHOR_END: factory
@@ -91,7 +87,7 @@ impl FilterFactory for GreetFilterFactory {
async fn main() {
quilkin::run(
quilkin::Config::builder().build(),
vec![self::factory()].into_iter(),
vec![Greet::factory()].into_iter(),
)
.await
.unwrap();
20 changes: 11 additions & 9 deletions src/config/config_type.rs
Original file line number Diff line number Diff line change
@@ -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.
@@ -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}",
@@ -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))
Loading

0 comments on commit 11f7649

Please sign in to comment.