Skip to content

Commit

Permalink
feat: added custom query-param extractor for platform queries
Browse files Browse the repository at this point in the history
  • Loading branch information
ShubhranshuSanjeev committed Sep 22, 2024
1 parent 1a54fd2 commit 84ae5a6
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 1 deletion.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ members = [
"examples/experimentation_client_integration_example",
"examples/cac_client_integration_example",
"examples/superposition-demo-app",
"crates/superposition_macros"
"crates/superposition_macros",
"crates/superposition_proc_macros"
]

[[workspace.metadata.leptos]]
Expand Down
1 change: 1 addition & 0 deletions crates/service_utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ once_cell = { workspace = true }
regex = { workspace = true }
mime = { workspace = true }
superposition_types = { path="../superposition_types" }
superposition_proc_macros = { path="../superposition_proc_macros" }
aws-sdk-kms = {workspace = true}
aws-config = {workspace = true}

Expand Down
7 changes: 7 additions & 0 deletions crates/service_utils/src/service/types.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use crate::db::pgschema_manager::{PgSchemaConnection, PgSchemaManager};
use derive_more::{Deref, DerefMut};
use jsonschema::JSONSchema;
use serde::de::DeserializeOwned;
use serde_json::{json, Map, Value};
use superposition_proc_macros::CustomQuery;
use superposition_types::custom_query::CustomQuery as CustomQueryTrait;

use std::{
collections::HashSet,
Expand Down Expand Up @@ -256,3 +259,7 @@ impl FromRequest for CustomHeaders {
ready(Ok(val))
}
}

#[derive(Debug, Clone, CustomQuery)]
#[custom_query(regex = r"platform\[(?<query_name>.*)\]", capture_group = "query_name")]
pub struct PlatformQuery<T: DeserializeOwned>(pub T);
17 changes: 17 additions & 0 deletions crates/superposition_proc_macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "superposition_proc_macros"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"

[lints]
workspace = true

[lib]
proc-macro = true
138 changes: 138 additions & 0 deletions crates/superposition_proc_macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::punctuated::Punctuated;
use syn::{parse::Parse, parse_macro_input, DeriveInput, Error, LitStr, Meta, Token};

#[proc_macro_derive(CustomQuery, attributes(custom_query))]
pub fn derive_custom_query(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let generics = &input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

let custom_query_attr = match extract_attributes(&input.attrs) {
Ok(Some(attr)) => attr,
Ok(None) => {
return Error::new_spanned(&input, "Missing #[custom_query] attribute")
.to_compile_error()
.into()
}
Err(e) => return e.to_compile_error().into(),
};

let regex_pattern = &custom_query_attr.regex;
let capture_group = &custom_query_attr.capture_group;

let inner_type = generics
.type_params()
.next()
.map(|t| t.ident.clone())
.unwrap_or_else(|| syn::Ident::new("T", proc_macro2::Span::call_site()));

let expanded = quote! {
impl #impl_generics superposition_types::custom_query::CustomQuery for #name #ty_generics #where_clause {
type Inner = #inner_type;

fn regex_pattern() -> &'static str {
#regex_pattern
}

fn capture_group() -> &'static str {
#capture_group
}

fn from_inner(inner: Self::Inner) -> Self {
Self(inner)
}
}

impl #impl_generics #name #ty_generics #where_clause {
pub fn into_inner(self) -> #inner_type {
self.0
}
}

impl #impl_generics actix_web::FromRequest for #name #ty_generics #where_clause {
type Error = actix_web::Error;
type Future = std::future::Ready<Result<Self, Self::Error>>;

fn from_request(req: &actix_web::HttpRequest, _: &mut actix_web::dev::Payload) -> Self::Future {
use std::future::ready;
ready(Self::extract_query(req.query_string()).map_err(actix_web::Error::from))
}
}
};

TokenStream::from(expanded)
}

struct CustomQueryAttr {
regex: LitStr,
capture_group: LitStr,
}

impl Parse for CustomQueryAttr {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let attrs = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;

let mut regex = None;
let mut capture_group = None;

for attr in attrs {
match attr {
Meta::NameValue(nv) if nv.path.is_ident("regex") => {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = nv.value
{
regex = Some(lit_str);
}
}
Meta::NameValue(nv) if nv.path.is_ident("capture_group") => {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = nv.value
{
capture_group = Some(lit_str);
}
}
_ => {
return Err(syn::Error::new_spanned(
attr,
"Expected `regex` or `capture_group`",
))
}
}
}

match (regex, capture_group) {
(Some(regex), Some(capture_group)) => Ok(CustomQueryAttr {
regex,
capture_group,
}),
(None, Some(_)) => {
Err(syn::Error::new(input.span(), "Missing `regex` parameter"))
}
(Some(_), None) => Err(syn::Error::new(
input.span(),
"Missing `capture_group` parameter",
)),
(None, None) => Err(syn::Error::new(
input.span(),
"Missing both `regex` and `capture_group` parameters",
)),
}
}
}

fn extract_attributes(
attrs: &[syn::Attribute],
) -> Result<Option<CustomQueryAttr>, Error> {
attrs
.iter()
.find(|attr| attr.path().is_ident("custom_query"))
.map(|attr| attr.parse_args::<CustomQueryAttr>())
.transpose()
}
45 changes: 45 additions & 0 deletions crates/superposition_types/src/custom_query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use std::collections::HashMap;

use actix_web::web::Query;
use regex::Regex;
use serde::de::DeserializeOwned;

pub trait CustomQuery: Sized {
type Inner: DeserializeOwned;

fn regex_pattern() -> &'static str;
fn capture_group() -> &'static str;

fn query_regex() -> Regex {
Regex::new(Self::regex_pattern()).unwrap()
}

fn extract_query(
query_string: &str,
) -> Result<Self, actix_web::error::QueryPayloadError> {
let query_map = Query::<HashMap<String, String>>::from_query(query_string)?;
let filtered_query = Self::filter_and_transform_query(query_map.into_inner());
let inner = Query::<Self::Inner>::from_query(&filtered_query)?.into_inner();
Ok(Self::from_inner(inner))
}

fn filter_and_transform_query(query_map: HashMap<String, String>) -> String {
let regex = Self::query_regex();
query_map
.into_iter()
.filter_map(|(k, v)| {
Self::extract_key(&regex, &k).map(|pk| format!("{pk}={v}"))
})
.collect::<Vec<String>>()
.join("&")
}

fn extract_key(regex: &Regex, key: &str) -> Option<String> {
regex
.captures(key)
.and_then(|captures| captures.name(Self::capture_group()))
.map(|m| m.as_str().to_owned())
}

fn from_inner(inner: Self::Inner) -> Self;
}
1 change: 1 addition & 0 deletions crates/superposition_types/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod custom_query;
pub mod result;
use std::fmt::Display;

Expand Down

0 comments on commit 84ae5a6

Please sign in to comment.