move serializable http types to a separate module (#633)

## 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
This commit is contained in:
Anon Ray 2024-05-30 13:34:04 +05:30 committed by hasura-bot
parent d7f1753c65
commit dd2e2847d3
3 changed files with 184 additions and 112 deletions

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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::{
@ -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)]
@ -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 {
@ -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;
@ -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 =