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 173acca
Show file tree
Hide file tree
Showing 36 changed files with 640 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
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
46 changes: 20 additions & 26 deletions examples/quilkin-filter-example/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
*/

// 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::*;

Expand All @@ -31,15 +30,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
Expand All @@ -60,28 +67,15 @@ impl Filter for Greet {
// ANCHOR_END: filter

// ANCHOR: factory
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
}
use quilkin::filters::StaticFilter;

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
Expand All @@ -91,7 +85,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();
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
Loading

0 comments on commit 173acca

Please sign in to comment.