Skip to content

Commit

Permalink
move serializable http types to a separate module (#633)
Browse files Browse the repository at this point in the history
## Description

We use few HTTP types (URL, http header name, value, header map etc.) in
the resolved metadata, which needs to be serialized. We have newtype
wrappers around them to implement serialization/deserialization.

This PR moves them to a separate helper module.

And also introduces a new type `SerializableHeaderName` which is
required in an upcoming PR of forwarding request headers to NDC.

Functional no-op.

V3_GIT_ORIGIN_REV_ID: 4f907a652a9826bc52996fa37d2a7590f24ee30a
  • Loading branch information
ecthiender authored and hasura-bot committed May 30, 2024
1 parent d7f1753 commit dd2e284
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 112 deletions.
178 changes: 178 additions & 0 deletions v3/crates/metadata-resolve/src/helpers/http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//! Wrapper over HTTP types which can serialized/deserialized
use indexmap::IndexMap;
use open_dds::EnvironmentValue;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde::{
de::Error as DeError,
ser::{Error as SerError, SerializeMap},
};
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, fmt::Display, str::FromStr};

#[derive(Debug)]
pub enum HeaderError {
InvalidHeaderName { header_name: String },
InvalidHeaderValue { header_name: String },
}

impl Display for HeaderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HeaderError::InvalidHeaderName { header_name } => {
write!(f, "invalid HTTP header name: {}", header_name)
}
HeaderError::InvalidHeaderValue { header_name } => {
write!(f, "invalid HTTP header value: {}", header_name)
}
}
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SerializableUrl(pub reqwest::Url);

impl SerializableUrl {
pub fn new(url: &str) -> Result<Self, url::ParseError> {
let url = reqwest::Url::parse(url)?;
Ok(Self(url))
}
}

impl Serialize for SerializableUrl {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.0.as_str())
}
}

impl<'de> Deserialize<'de> for SerializableUrl {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let url_str = String::deserialize(deserializer)?;
let url = reqwest::Url::parse(&url_str)
.map_err(|_| D::Error::custom(format!("Invalid URL: {url_str}")))?;
Ok(SerializableUrl(url))
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SerializableHeaderName(pub HeaderName);

#[allow(dead_code)]
impl SerializableHeaderName {
pub fn new(header_name_str: String) -> Result<Self, HeaderError> {
let header_name =
HeaderName::from_str(&header_name_str).map_err(|_| HeaderError::InvalidHeaderName {
header_name: header_name_str,
})?;
Ok(SerializableHeaderName(header_name))
}
}

impl Serialize for SerializableHeaderName {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.0.as_str())
}
}

impl<'de> Deserialize<'de> for SerializableHeaderName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let header_str = String::deserialize(deserializer)?;
SerializableHeaderName::new(header_str).map_err(D::Error::custom)
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SerializableHeaderMap(pub HeaderMap);

impl SerializableHeaderMap {
pub fn new(headers: &IndexMap<String, EnvironmentValue>) -> Result<Self, HeaderError> {
let header_map = headers
.iter()
.map(|(k, v)| {
Ok((
HeaderName::from_str(k).map_err(|_| HeaderError::InvalidHeaderName {
header_name: k.clone(),
})?,
HeaderValue::from_str(&v.value).map_err(|_| {
HeaderError::InvalidHeaderValue {
header_name: k.clone(),
}
})?,
))
})
.collect::<Result<HeaderMap, HeaderError>>()?;
Ok(Self(header_map))
}
}

impl Serialize for SerializableHeaderMap {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for (k, v) in &self.0 {
map.serialize_entry(k.as_str(), v.to_str().map_err(S::Error::custom)?)?;
}
map.end()
}
}

impl<'de> Deserialize<'de> for SerializableHeaderMap {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let hash_map = BTreeMap::<String, String>::deserialize(deserializer)?;
let header_map: HeaderMap = hash_map
.into_iter()
.map(|(k, v)| {
Ok((
HeaderName::from_str(&k).map_err(D::Error::custom)?,
HeaderValue::from_str(&v).map_err(D::Error::custom)?,
))
})
.collect::<Result<HeaderMap, D::Error>>()?;
Ok(SerializableHeaderMap(header_map))
}
}

#[cfg(test)]
mod tests {
use serde_json::json;

use super::*;

#[test]
fn test_header_name_serialization_deserialization() {
let headers_str = json!([
"ABCDEFGHIJKLMNOPQRSTUVWXYZ-abcdefghijklmnopqrstuvwxyz0123456789_#.~!$&'*+",
"ABCDEFGHIJKLMNOPQRSTUVWXYZ-abcdefghijklmnopqrstuvwxyz0123456789_#.~!$&'*+",
]);
let headers: Vec<SerializableHeaderName> = serde_json::from_value(headers_str).unwrap();
let serialized_headers = serde_json::to_string(&headers).unwrap();
let deserialized_headers: Vec<SerializableHeaderName> =
serde_json::from_str(&serialized_headers).unwrap();
assert_eq!(deserialized_headers, headers);
}

#[test]
fn test_header_map_serialization_deserialization() {
let headers_str = r#"{"some_header_name":"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!#$&'()*+,/:;=?@[]\""}"#;
let headers: SerializableHeaderMap = serde_json::from_str(headers_str).unwrap();
let serialized_headers = serde_json::to_string(&headers).unwrap();
assert_eq!(headers_str, serialized_headers);
}
}
1 change: 1 addition & 0 deletions v3/crates/metadata-resolve/src/helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Functions that are shared between metadata stages
pub mod argument;
pub mod http;
pub mod model;
pub mod ndc_validation;
pub mod type_mappings;
Expand Down
117 changes: 5 additions & 112 deletions v3/crates/metadata-resolve/src/stages/data_connectors/types.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::helpers::http::{HeaderError, SerializableHeaderMap, SerializableUrl};
use crate::types::error::Error;
use crate::types::subgraph::Qualified;
use serde::{Deserialize, Serialize};

use indexmap::IndexMap;

use lang_graphql::ast::common::OperationType;
use ndc_models;
use open_dds::{
commands::{FunctionName, ProcedureName},
data_connector::{
Expand All @@ -12,15 +12,8 @@ use open_dds::{
},
EnvironmentValue,
};

use lang_graphql::ast::common::OperationType;
use ndc_models;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde::{
de::Error as DeError,
ser::{Error as SerError, SerializeMap},
};
use std::{collections::BTreeMap, str::FromStr};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

/// information that does not change between resolver stages
#[derive(Clone)]
Expand Down Expand Up @@ -209,9 +202,6 @@ impl DataConnectorLink {
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SerializableUrl(pub reqwest::Url);

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ResolvedReadWriteUrls {
Expand Down Expand Up @@ -241,95 +231,6 @@ impl ResolvedDataConnectorUrl {
}
}

impl SerializableUrl {
pub fn new(url: &str) -> Result<Self, url::ParseError> {
let url = reqwest::Url::parse(url)?;
Ok(Self(url))
}
}

impl Serialize for SerializableUrl {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.0.as_str())
}
}

impl<'de> Deserialize<'de> for SerializableUrl {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let url_str = String::deserialize(deserializer)?;
let url = reqwest::Url::parse(&url_str)
.map_err(|_| D::Error::custom(format!("Invalid URL: {url_str}")))?;
Ok(SerializableUrl(url))
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SerializableHeaderMap(pub HeaderMap);

pub enum HeaderError {
InvalidHeaderName { header_name: String },
InvalidHeaderValue { header_name: String },
}

impl SerializableHeaderMap {
fn new(headers: &IndexMap<String, EnvironmentValue>) -> Result<Self, HeaderError> {
let header_map = headers
.iter()
.map(|(k, v)| {
Ok((
HeaderName::from_str(k).map_err(|_| HeaderError::InvalidHeaderName {
header_name: k.clone(),
})?,
HeaderValue::from_str(&v.value).map_err(|_| {
HeaderError::InvalidHeaderValue {
header_name: k.clone(),
}
})?,
))
})
.collect::<Result<HeaderMap, HeaderError>>()?;
Ok(Self(header_map))
}
}

impl Serialize for SerializableHeaderMap {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for (k, v) in &self.0 {
map.serialize_entry(k.as_str(), v.to_str().map_err(S::Error::custom)?)?;
}
map.end()
}
}

impl<'de> Deserialize<'de> for SerializableHeaderMap {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let hash_map = BTreeMap::<String, String>::deserialize(deserializer)?;
let header_map: HeaderMap = hash_map
.into_iter()
.map(|(k, v)| {
Ok((
HeaderName::from_str(&k).map_err(D::Error::custom)?,
HeaderValue::from_str(&v).map_err(D::Error::custom)?,
))
})
.collect::<Result<HeaderMap, D::Error>>()?;
Ok(SerializableHeaderMap(header_map))
}
}

#[cfg(test)]
mod tests {
use ndc_models;
Expand All @@ -353,14 +254,6 @@ mod tests {
assert_eq!(actual_url_str, serialized_url);
}

#[test]
fn test_header_map_serialization_deserialization() {
let headers_str = r#"{"name":"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!#$&'()*+,/:;=?@[]\""}"#;
let headers: super::SerializableHeaderMap = serde_json::from_str(headers_str).unwrap();
let serialized_headers = serde_json::to_string(&headers).unwrap();
assert_eq!(headers_str, serialized_headers);
}

#[test]
fn test_data_connector_context_capablities() {
let data_connector_with_capabilities: DataConnectorLinkV1 =
Expand Down

0 comments on commit dd2e284

Please sign in to comment.