Replace lazy_static with stdlib equivalents. (#758)

### What

The `lazy_static` macro is poorly maintained, fairly bloated, and has
been mostly superseded by
[`OnceLock`](https://doc.rust-lang.org/stable/std/sync/struct.OnceLock.html)
in the stdlib.

### How

1. I turned a couple of `static ref` values into `const`, sometimes by
creating `const fn` equivalents to other functions.
2. I inlined static behavior to construct a JSON pointer into some
tests, where we don't care too much about losing a few milliseconds.
3. For the rest, I replaced `lazy_static` with a `static OnceLock` and a
call to `OnceLock::get_or_init`.

V3_GIT_ORIGIN_REV_ID: 18e4150a5fb24fe71f6ed77fe6178b7942405aa3
This commit is contained in:
Samir Talwar 2024-06-26 14:44:57 +02:00 committed by hasura-bot
parent 82c0c65bd0
commit 5d619a540f
19 changed files with 218 additions and 227 deletions

6
v3/Cargo.lock generated
View File

@ -2170,7 +2170,6 @@ dependencies = [
"jsonwebkey",
"jsonwebtoken",
"lang-graphql",
"lazy_static",
"mockito",
"open-dds",
"openssl",
@ -2191,7 +2190,6 @@ dependencies = [
"axum",
"hasura-authn-core",
"lang-graphql",
"lazy_static",
"mockito",
"open-dds",
"rand",
@ -2598,7 +2596,6 @@ dependencies = [
"http",
"human_bytes",
"indexmap 2.2.6",
"lazy_static",
"lexical-core",
"nonempty",
"postcard",
@ -2783,7 +2780,6 @@ dependencies = [
"indexmap 2.2.6",
"insta",
"lang-graphql",
"lazy_static",
"ndc-models",
"nonempty",
"open-dds",
@ -3062,7 +3058,6 @@ dependencies = [
"goldenfile",
"indexmap 2.2.6",
"jsonschema-tidying",
"lazy_static",
"ndc-models",
"opendds-derive",
"pretty_assertions",
@ -3082,7 +3077,6 @@ version = "0.1.0"
dependencies = [
"convert_case 0.6.0",
"darling",
"lazy_static",
"proc-macro2",
"quote",
"regex",

View File

@ -82,7 +82,6 @@ json_value_merge = "2"
jsonptr = "0.4"
jsonwebkey = "0.3"
jsonwebtoken = "8"
lazy_static = "1"
lexical-core = "0.8"
mockito = { version = "1", default-features = false }
nonempty = "0.10"

View File

@ -179,7 +179,7 @@ pub async fn resolve_session<'a, B>(
Ok(h) => SessionVariableValue::new(h),
};
if session_variable == SESSION_VARIABLE_ROLE.to_owned() {
if session_variable == SESSION_VARIABLE_ROLE {
role = Some(Role::new(&variable_value.0));
} else {
// TODO: Handle the duplicate case?

View File

@ -17,7 +17,6 @@ axum = { workspace = true }
cookie = { workspace = true }
jsonptr = { workspace = true }
jsonwebtoken = { workspace = true }
lazy_static = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
schemars = { workspace = true, features = ["url"] }
serde = { workspace = true, features = ["derive"] }

View File

@ -148,7 +148,7 @@ mod tests {
) -> anyhow::Result<String> {
let claims: Claims = get_claims(
&serde_json::to_value(hasura_claims)?,
&DEFAULT_HASURA_CLAIMS_NAMESPACE_POINTER,
jsonptr::Pointer::new([DEFAULT_HASURA_CLAIMS_NAMESPACE]).as_str(),
)?;
let jwt_header = jwt::Header {
alg,
@ -180,27 +180,25 @@ mod tests {
async fn test_unsuccessful_role_emulation() -> anyhow::Result<()> {
let encoded_claims = get_encoded_claims(Algorithm::HS256, &get_default_hasura_claims())?;
let jwt_secret_config_json = json!(
{
"key": {
"fixed": {
let jwt_secret_config_json = json!({
"key": {
"fixed": {
"algorithm": "HS256",
"key": {
"value": "token"
}
}
},
"tokenLocation": {
"type": "BearerAuthorization"
},
"claimsConfig": {
"namespace": {
"claimsFormat": "Json",
"location": *DEFAULT_HASURA_CLAIMS_NAMESPACE_POINTER
}
}
}
);
"value": "token"
},
},
},
"tokenLocation": {
"type": "BearerAuthorization",
},
"claimsConfig": {
"namespace": {
"claimsFormat": "Json",
"location": jsonptr::Pointer::new([DEFAULT_HASURA_CLAIMS_NAMESPACE]),
},
},
});
let jwt_config: JWTConfig = serde_json::from_value(jwt_secret_config_json)?;
@ -279,27 +277,25 @@ mod tests {
);
let encoded_claims = get_encoded_claims(Algorithm::HS256, &hasura_claims)?;
let jwt_secret_config_json = json!(
{
"key": {
"fixed": {
let jwt_secret_config_json = json!({
"key": {
"fixed": {
"algorithm": "HS256",
"key": {
"value": "token"
}
}
},
"tokenLocation": {
"type": "BearerAuthorization"
},
"claimsConfig": {
"namespace": {
"claimsFormat": "Json",
"location": *DEFAULT_HASURA_CLAIMS_NAMESPACE_POINTER
}
}
}
);
"value": "token",
},
}
},
"tokenLocation": {
"type": "BearerAuthorization",
},
"claimsConfig": {
"namespace": {
"claimsFormat": "Json",
"location": jsonptr::Pointer::new([DEFAULT_HASURA_CLAIMS_NAMESPACE]),
},
},
});
let jwt_config: JWTConfig = serde_json::from_value(jwt_secret_config_json)?;

View File

@ -8,7 +8,6 @@ use hasura_authn_core::{Role, SessionVariable, SessionVariableValue};
use jsonptr::Pointer;
use jsonwebtoken::{self as jwt, decode, DecodingKey, Validation};
use jwt::decode_header;
use lazy_static::lazy_static;
use reqwest::header::{AUTHORIZATION, COOKIE};
use reqwest::StatusCode;
use schemars::gen::SchemaGenerator;
@ -24,14 +23,7 @@ use url::Url;
/// Name of the key, which is by default used to lookup the Hasura claims
/// in the claims obtained after decoding the JWT.
const DEFAULT_HASURA_CLAIMS_NAMESPACE: &str = "claims.jwt.hasura.io";
lazy_static! {
/// Make top level JSON pointer of the `DEFAULT_HASURA_CLAIMS_NAMESPACE`
/// by escaping the `/` by `~1`.
pub(crate) static ref DEFAULT_HASURA_CLAIMS_NAMESPACE_POINTER: String =
"/".to_owned() + &DEFAULT_HASURA_CLAIMS_NAMESPACE.replace('/', "~1");
}
pub(crate) const DEFAULT_HASURA_CLAIMS_NAMESPACE: &str = "claims.jwt.hasura.io";
#[derive(Debug, Error)]
pub enum Error {
@ -361,7 +353,7 @@ impl JWTConfig {
"claimsConfig": {
"namespace": {
"claimsFormat": "Json",
"location": *DEFAULT_HASURA_CLAIMS_NAMESPACE_POINTER
"location": jsonptr::Pointer::new([DEFAULT_HASURA_CLAIMS_NAMESPACE]),
}
}
}
@ -723,7 +715,7 @@ mod tests {
let hasura_claims = get_default_hasura_claims();
let claims: Claims = get_claims(
&serde_json::to_value(hasura_claims)?,
&DEFAULT_HASURA_CLAIMS_NAMESPACE_POINTER,
jsonptr::Pointer::new([DEFAULT_HASURA_CLAIMS_NAMESPACE]).as_str(),
)?;
let jwt_header = jwt::Header {
alg,
@ -851,7 +843,7 @@ mod tests {
"claimsConfig": {
"namespace": {
"claimsFormat": "Json",
"location": *DEFAULT_HASURA_CLAIMS_NAMESPACE_POINTER
"location": jsonptr::Pointer::new([DEFAULT_HASURA_CLAIMS_NAMESPACE]),
}
}
}
@ -886,7 +878,7 @@ mod tests {
"claimsConfig": {
"namespace": {
"claimsFormat": "StringifiedJson",
"location": *DEFAULT_HASURA_CLAIMS_NAMESPACE_POINTER
"location": jsonptr::Pointer::new([DEFAULT_HASURA_CLAIMS_NAMESPACE]),
}
}
}
@ -898,7 +890,7 @@ mod tests {
let stringified_hasura_claims = serde_json::to_string(&hasura_claims)?;
let claims = get_claims(
&serde_json::to_value(stringified_hasura_claims)?,
&DEFAULT_HASURA_CLAIMS_NAMESPACE_POINTER,
jsonptr::Pointer::new([DEFAULT_HASURA_CLAIMS_NAMESPACE]).as_str(),
)?;
let alg = jwt::Algorithm::HS256;
let jwt_header = jwt::Header {
@ -1105,7 +1097,7 @@ mod tests {
let claims: Claims = get_claims(
&serde_json::to_value(&hasura_claims)?,
&DEFAULT_HASURA_CLAIMS_NAMESPACE_POINTER,
jsonptr::Pointer::new([DEFAULT_HASURA_CLAIMS_NAMESPACE]).as_str(),
)?;
let mut jwt_header = jwt::Header::new(jwt::Algorithm::ES256);
@ -1119,21 +1111,20 @@ mod tests {
let jwk_url = url + "/jwk";
let jwt_config_json = json!(
{
"key": {
"jwkFromUrl": jwk_url
let jwt_config_json = json!({
"key": {
"jwkFromUrl": jwk_url,
},
"tokenLocation": {
"type": "BearerAuthorization",
},
"claimsConfig": {
"namespace": {
"claimsFormat": "Json",
"location": jsonptr::Pointer::new([DEFAULT_HASURA_CLAIMS_NAMESPACE]),
},
"tokenLocation": {
"type": "BearerAuthorization"
},
"claimsConfig": {
"namespace": {
"claimsFormat": "Json",
"location": *DEFAULT_HASURA_CLAIMS_NAMESPACE_POINTER
}
}
}
},
}
);
let jwt_config: JWTConfig = serde_json::from_value(jwt_config_json)?;
@ -1334,23 +1325,21 @@ mod tests {
let hasura_claims = get_default_hasura_claims();
let claims: Claims = get_claims(
&serde_json::to_value(hasura_claims.clone())?,
&DEFAULT_HASURA_CLAIMS_NAMESPACE_POINTER,
jsonptr::Pointer::new([DEFAULT_HASURA_CLAIMS_NAMESPACE]).as_str(),
)?;
let encoded_claims = encode(&jwt_header, &claims, &encoding_key)?;
let jwt_secret_config_json = json!(
{
"key": jwt_key_config,
"tokenLocation": {
"type": "BearerAuthorization"
let jwt_secret_config_json = json!({
"key": jwt_key_config,
"tokenLocation": {
"type": "BearerAuthorization"
},
"claimsConfig": {
"namespace": {
"claimsFormat": "Json",
"location": jsonptr::Pointer::new([DEFAULT_HASURA_CLAIMS_NAMESPACE]),
},
"claimsConfig": {
"namespace": {
"claimsFormat": "Json",
"location": *DEFAULT_HASURA_CLAIMS_NAMESPACE_POINTER
}
}
}
);
},
});
let jwt_config: JWTConfig = serde_json::from_value(jwt_secret_config_json)?;
let http_client = reqwest::Client::new();
let decoded_claims =

View File

@ -14,8 +14,7 @@ open-dds = { path = "../../open-dds" }
tracing-util = { path = "../../utils/tracing-util" }
axum = { workspace = true }
lazy_static = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
reqwest = { workspace = true, features = ["json"] }
schemars = { workspace = true, features = ["smol_str", "url"] }
serde = { workspace = true }
serde_json = { workspace = true }

View File

@ -1,15 +1,13 @@
use std::{
collections::{HashMap, HashSet},
str::FromStr,
time::Duration,
};
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use std::sync::OnceLock;
use std::time::Duration;
use auth_base::{Identity, Role, RoleAuthorization, SessionVariable, SessionVariableValue};
use axum::{
http::{HeaderMap, HeaderName, StatusCode},
response::IntoResponse,
};
use lazy_static::lazy_static;
use reqwest::{header::ToStrError, Url};
use serde::{de::Error as SerdeDeError, Deserialize, Deserializer, Serialize, Serializer};
@ -78,29 +76,6 @@ impl IntoResponse for Error {
}
}
lazy_static! {
/// Ignore the following list of request headers, sent by the client
/// when making a GET request to the auth hook. Note that, in the case
/// the auth hook mode is `POST`, this is *not* applicable i.e. all the
/// headers sent by the client are forwarded to the auth hook.
static ref COMMON_CLIENT_HEADERS_TO_IGNORE: HashSet<String> = HashSet::from([
"Content-Length",
"Content-MD5",
"User-Agent",
"Host",
"Origin",
"Referer",
"Accept",
"Accept-Encoding",
"Accept-Language",
"Accept-Datetime",
"Cache-Control",
"Connection",
"DNT",
"Content-Type",
].map(str::to_lowercase));
}
fn serialize_url<S>(url: &Url, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@ -171,7 +146,7 @@ async fn make_auth_hook_request(
AuthHookMethod::Get => {
let mut auth_hook_headers = tracing_util::get_trace_headers();
for (header_name, header_value) in client_headers {
if !COMMON_CLIENT_HEADERS_TO_IGNORE.contains(header_name.as_str()) {
if !ignore_header(header_name.as_str()) {
auth_hook_headers.insert(header_name, header_value.clone());
}
}
@ -306,6 +281,41 @@ pub async fn authenticate_request(
.await
}
/// Ignore the following list of request headers, sent by the client when making a GET request to
/// the auth hook.
///
/// Note that, in the case the auth hook mode is `POST`, this is *not* applicable, i.e. all the
/// headers sent by the client are forwarded to the auth hook.
const COMMON_CLIENT_HEADERS_TO_IGNORE: [&str; 14] = [
"Accept",
"Accept-Datetime",
"Accept-Encoding",
"Accept-Language",
"Cache-Control",
"Connection",
"Content-Length",
"Content-MD5",
"Content-Type",
"DNT",
"Host",
"Origin",
"Referer",
"User-Agent",
];
/// Decides whether to ignore the given header sent by the client when making a GET request to
/// the auth hook.
///
/// The header is compared to a static list of headers (above).
///
/// Note that, in the case the auth hook mode is `POST`, this is *not* applicable, i.e. all the
/// headers sent by the client are forwarded to the auth hook.
fn ignore_header(header: &str) -> bool {
static CELL: OnceLock<HashSet<String>> = OnceLock::new();
CELL.get_or_init(|| HashSet::from(COMMON_CLIENT_HEADERS_TO_IGNORE.map(str::to_lowercase)))
.contains(header)
}
#[cfg(test)]
mod tests {
use std::str::FromStr;

View File

@ -37,7 +37,6 @@ serde_json = { workspace = true }
serde_with = { workspace = true }
smol_str = { workspace = true }
thiserror = { workspace = true }
lazy_static = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }

View File

@ -3,6 +3,8 @@ This module provides functions to generate introspection result as GraphQL schem
for each namespace from the schema.
*/
use std::collections::HashMap;
use std::sync::OnceLock;
use thiserror::Error;
#[derive(Error, Debug)]
@ -21,24 +23,6 @@ pub enum Error {
SerializeJson(#[from] serde_json::Error),
}
lazy_static::lazy_static! {
static ref INTROSPECTION_REQUEST: Result<crate::http::Request, String> = {
Ok(
crate::http::Request {
operation_name: None,
query: {
let query_str = include_str!("introspection_query.graphql");
crate::parser::Parser::new(query_str)
.parse_executable_document()
.map_err(|e| e.to_string())?
},
variables: HashMap::new(),
}
)
};
}
/// Generate GraphQL schema for a given namespace
pub fn build_namespace_schema<
S: crate::schema::SchemaContext,
@ -47,12 +31,9 @@ pub fn build_namespace_schema<
namespaced_getter: &NSGet,
schema: &crate::schema::Schema<S>,
) -> Result<serde_json::Value, Error> {
let request = match &(*INTROSPECTION_REQUEST) {
Ok(req) => req,
Err(e) => Err(Error::ParseIntrospectionQuery((*e).clone()))?,
};
let nr = crate::validation::normalize_request(namespaced_getter, schema, request)
.map_err(|e| Error::NormalizeIntrospectionQuery(e.to_string()))?;
let nr =
crate::validation::normalize_request(namespaced_getter, schema, introspection_request())
.map_err(|e| Error::NormalizeIntrospectionQuery(e.to_string()))?;
let mut result = HashMap::new();
for (_alias, field) in &nr.selection_set.fields {
let field_call = field.field_call().map_err(|_| Error::FieldCallNotFound)?;
@ -74,3 +55,17 @@ pub fn build_namespace_schema<
}
Ok(serde_json::to_value(result)?)
}
fn introspection_request() -> &'static crate::http::Request {
static CELL: OnceLock<crate::http::Request> = OnceLock::new();
CELL.get_or_init(|| crate::http::Request {
operation_name: None,
query: {
let query_str = include_str!("introspection_query.graphql");
crate::parser::Parser::new(query_str)
.parse_executable_document()
.unwrap()
},
variables: HashMap::new(),
})
}

View File

@ -14,7 +14,6 @@ open-dds = { path = "../open-dds" }
derive_more = { workspace = true }
indexmap = { workspace = true, features = ["serde"] }
lazy_static = { workspace = true }
ndc-models = { workspace = true }
nonempty = { workspace = true }
ref-cast = { workspace = true }

View File

@ -1,6 +1,7 @@
//! This is where we will resolve graphql configuration
use std::collections::HashSet;
use std::sync::OnceLock;
use crate::helpers::types::mk_name;
use crate::types::error::{Error, GraphqlConfigError};
@ -10,51 +11,6 @@ use open_dds::graphql_config::{self, OrderByDirection};
use open_dds::types::GraphQlFieldName;
use serde::{Deserialize, Serialize};
lazy_static::lazy_static! {
static ref FALLBACK_GRAPHQL_CONFIG: graphql_config::GraphqlConfig = graphql_config::GraphqlConfig::V1(graphql_config::GraphqlConfigV1 {
query: graphql_config::QueryGraphqlConfig {
root_operation_type_name: "Query".to_string(),
arguments_input: Some (graphql_config::ArgumentsInputGraphqlConfig {
field_name: "args".to_string(),
}),
limit_input: Some(graphql_config::LimitInputGraphqlConfig {
field_name: "limit".to_string(),
}),
offset_input: Some(graphql_config::OffsetInputGraphqlConfig {
field_name: "offset".to_string(),
}),
filter_input: Some(graphql_config::FilterInputGraphqlConfig {
field_name: "where".to_string(),
operator_names: graphql_config::FilterInputOperatorNames {
and: "_and".to_string(),
or: "_or".to_string(),
not: "_not".to_string(),
is_null: "_is_null".to_string(),
},
}),
order_by_input: Some(graphql_config::OrderByInputGraphqlConfig {
field_name:"order_by".to_string(),
enum_direction_values: graphql_config::OrderByDirectionValues {
asc: "Asc".to_string(),
desc: "Desc".to_string(),
},
enum_type_names: vec![graphql_config::OrderByEnumTypeName{
type_name: "order_by".to_string(),
directions: vec![graphql_config::OrderByDirection::Asc, graphql_config::OrderByDirection::Desc],
}] }),
aggregate: Some(graphql_config::AggregateGraphqlConfig {
filter_input_field_name: GraphQlFieldName("filter_input".to_string()),
count_field_name: GraphQlFieldName("_count".to_string()),
count_distinct_field_name: GraphQlFieldName("_count_distinct".to_string()),
})
},
mutation: graphql_config::MutationGraphqlConfig{
root_operation_type_name: "Mutation".to_string(),
},
apollo_federation: None,
});
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct GraphqlConfig {
// The graphql configuration that needs to be applied to each model, depending on it's conditions
@ -79,7 +35,7 @@ pub struct GraphqlConfig {
/// * if the flag is not set, use the fallback object
pub fn resolve(
graphql_configs: &Vec<QualifiedObject<graphql_config::GraphqlConfig>>,
flags: &open_dds::flags::Flags,
flags: open_dds::flags::Flags,
) -> Result<GraphqlConfig, Error> {
if graphql_configs.is_empty() {
if flags.require_graphql_config {
@ -87,8 +43,7 @@ pub fn resolve(
graphql_config_error: GraphqlConfigError::MissingGraphqlConfig,
});
}
let graphql_config = resolve_graphql_config(&FALLBACK_GRAPHQL_CONFIG)?;
Ok(graphql_config)
resolve_graphql_config(fallback_graphql_config())
} else {
match graphql_configs.as_slice() {
// There should only be one graphql config in supergraph
@ -305,3 +260,55 @@ pub fn resolve_graphql_config(
}
}
}
fn fallback_graphql_config() -> &'static graphql_config::GraphqlConfig {
static CELL: OnceLock<graphql_config::GraphqlConfig> = OnceLock::new();
CELL.get_or_init(|| {
graphql_config::GraphqlConfig::V1(graphql_config::GraphqlConfigV1 {
query: graphql_config::QueryGraphqlConfig {
root_operation_type_name: "Query".to_string(),
arguments_input: Some(graphql_config::ArgumentsInputGraphqlConfig {
field_name: "args".to_string(),
}),
limit_input: Some(graphql_config::LimitInputGraphqlConfig {
field_name: "limit".to_string(),
}),
offset_input: Some(graphql_config::OffsetInputGraphqlConfig {
field_name: "offset".to_string(),
}),
filter_input: Some(graphql_config::FilterInputGraphqlConfig {
field_name: "where".to_string(),
operator_names: graphql_config::FilterInputOperatorNames {
and: "_and".to_string(),
or: "_or".to_string(),
not: "_not".to_string(),
is_null: "_is_null".to_string(),
},
}),
order_by_input: Some(graphql_config::OrderByInputGraphqlConfig {
field_name: "order_by".to_string(),
enum_direction_values: graphql_config::OrderByDirectionValues {
asc: "Asc".to_string(),
desc: "Desc".to_string(),
},
enum_type_names: vec![graphql_config::OrderByEnumTypeName {
type_name: "order_by".to_string(),
directions: vec![
graphql_config::OrderByDirection::Asc,
graphql_config::OrderByDirection::Desc,
],
}],
}),
aggregate: Some(graphql_config::AggregateGraphqlConfig {
filter_input_field_name: GraphQlFieldName("filter_input".to_string()),
count_field_name: GraphQlFieldName("_count".to_string()),
count_distinct_field_name: GraphQlFieldName("_count_distinct".to_string()),
}),
},
mutation: graphql_config::MutationGraphqlConfig {
root_operation_type_name: "Mutation".to_string(),
},
apollo_federation: None,
})
})
}

View File

@ -33,7 +33,7 @@ pub fn resolve(
// The graphql config represents the shape of the Hasura features in the graphql schema,
// and which features should be enabled or disabled. We check this structure is valid.
let graphql_config =
graphql_config::resolve(&metadata_accessor.graphql_config, &metadata_accessor.flags)?;
graphql_config::resolve(&metadata_accessor.graphql_config, metadata_accessor.flags)?;
// Fetch and check schema information for all our data connectors
let data_connectors = data_connectors::resolve(&metadata_accessor)?;

View File

@ -13,7 +13,6 @@ opendds-derive = { path = "../utils/opendds-derive" }
derive_more = { workspace = true }
indexmap = { workspace = true, features = ["serde"] }
lazy_static = { workspace = true }
ndc-models = { workspace = true }
ref-cast = { workspace = true }
schemars = { workspace = true, features = ["smol_str", "preserve_order"] }

View File

@ -19,9 +19,7 @@ impl<T> QualifiedObject<T> {
}
}
lazy_static::lazy_static! {
static ref DEFAULT_FLAGS: flags::Flags = flags::Flags::default();
}
const DEFAULT_FLAGS: flags::Flags = flags::Flags::new();
pub struct MetadataAccessor {
pub data_connectors: Vec<QualifiedObject<data_connector::DataConnectorLinkV1>>,
@ -199,7 +197,7 @@ impl MetadataAccessor {
relationships: vec![],
commands: vec![],
command_permissions: vec![],
flags: flags.unwrap_or_else(|| DEFAULT_FLAGS.clone()),
flags: flags.unwrap_or(DEFAULT_FLAGS),
graphql_config: vec![],
}
}

View File

@ -1,6 +1,6 @@
use serde::Serialize;
#[derive(Serialize, Clone, Debug, Default, PartialEq, opendds_derive::OpenDd)]
#[derive(Debug, Clone, Copy, PartialEq, Serialize, opendds_derive::OpenDd)]
#[opendd(json_schema(rename = "OpenDdFlags"))]
pub struct Flags {
#[opendd(default, rename = "require_graphql_config")]
@ -9,9 +9,20 @@ pub struct Flags {
}
impl Flags {
// This is separated from `Default` so we can use a `const` implementation.
pub const fn new() -> Self {
Self {
require_graphql_config: false,
}
}
pub fn default_json() -> serde_json::Value {
serde_json::json!({
"require_graphql_config": false
})
serde_json::to_value(Self::new()).unwrap()
}
}
impl Default for Flags {
fn default() -> Self {
Self::new()
}
}

View File

@ -1,9 +1,9 @@
/// Module that defines the Hasura session variables.
use core::fmt;
//! Module that defines the Hasura session variables.
use std::borrow::Cow;
use std::convert::Infallible;
use std::str::FromStr;
use lazy_static::lazy_static;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -11,23 +11,20 @@ use serde::{Deserialize, Serialize};
/// "x-hasura-role".
#[derive(Debug, Clone, Hash, PartialEq, Eq, JsonSchema, Serialize, Deserialize)]
#[schemars(rename = "OpenDdSessionVariable")]
pub struct SessionVariable(String);
pub struct SessionVariable(Cow<'static, str>);
lazy_static! {
pub static ref SESSION_VARIABLE_ROLE: SessionVariable =
SessionVariable("x-hasura-role".to_string());
}
pub const SESSION_VARIABLE_ROLE: SessionVariable = SessionVariable(Cow::Borrowed("x-hasura-role"));
impl FromStr for SessionVariable {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(SessionVariable(s.trim().to_lowercase()))
Ok(SessionVariable(s.trim().to_lowercase().into()))
}
}
impl fmt::Display for SessionVariable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
impl std::fmt::Display for SessionVariable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
@ -57,8 +54,8 @@ mod tests {
fn serialize_and_deserialize_session_variable() {
let mut session_variables = HashMap::new();
session_variables.insert(
SessionVariable("test-role".to_string()),
SessionVariableValue("test-role".to_string()),
SessionVariable("test-role".into()),
SessionVariableValue("test-role".into()),
);
let json_str = serde_json::to_string(&session_variables).unwrap();

View File

@ -10,7 +10,6 @@ bench = false
[dependencies]
convert_case = { workspace = true }
darling = { workspace = true }
lazy_static = { workspace = true }
proc-macro2 = { workspace = true }
quote = { workspace = true }
regex = { workspace = true }

View File

@ -1,14 +1,11 @@
use std::sync::OnceLock;
use convert_case::{Case, Casing};
use darling::{FromAttributes, FromField, FromMeta};
use syn::{self, DeriveInput};
use crate::MacroResult;
lazy_static::lazy_static! {
/// Characters that we reject when constructing titles from schema names.
static ref INVALID_NAME_CHARACTER: regex::Regex = regex::Regex::new("[^0-9A-Za-z_]").unwrap();
}
/// JSON schema attributes
#[derive(Default, FromMeta)]
#[darling(default)]
@ -82,6 +79,10 @@ pub struct JsonSchemaMetadata {
impl<'a> Container<'a> {
pub fn from_derive_input(input: &'a DeriveInput) -> MacroResult<Self> {
static INVALID_NAME_CHARACTER: OnceLock<regex::Regex> = OnceLock::new();
let invalid_name_character =
INVALID_NAME_CHARACTER.get_or_init(|| regex::Regex::new("[^0-9A-Za-z_]").unwrap());
let (doc_title, doc_description) =
crate::helpers::get_title_and_desc_from_doc(&input.attrs);
let (json_schema_opts, data) = match &input.data {
@ -117,7 +118,7 @@ impl<'a> Container<'a> {
let schema_id = format!("https://hasura.io/jsonschemas/metadata/{id}");
let schema_name = json_schema_opts
.rename
.unwrap_or_else(|| INVALID_NAME_CHARACTER.replace_all(&id, "_").to_string());
.unwrap_or_else(|| invalid_name_character.replace_all(&id, "_").to_string());
let schema_title = json_schema_opts
.title
.or(doc_title)