Skip to content

Commit

Permalink
feat: support for default values in InputFieldDefinition (#2117)
Browse files Browse the repository at this point in the history
Co-authored-by: Tushar Mathur <tusharmath@gmail.com>
  • Loading branch information
shashitnak and tusharmath committed Jun 14, 2024
1 parent 55070c2 commit c4dfd8b
Show file tree
Hide file tree
Showing 16 changed files with 328 additions and 20 deletions.
3 changes: 3 additions & 0 deletions generated/.tailcallrc.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,9 @@
}
]
},
"default_value": {
"description": "Stores the default value for the field"
},
"doc": {
"description": "Publicly visible documentation for the field.",
"type": [
Expand Down
1 change: 1 addition & 0 deletions src/core/blueprint/blueprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ pub struct FieldDefinition {
pub resolver: Option<IR>,
pub directives: Vec<Directive>,
pub description: Option<String>,
pub default_value: Option<serde_json::Value>,
}

impl FieldDefinition {
Expand Down
3 changes: 2 additions & 1 deletion src/core/blueprint/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub fn to_input_object_type_definition(
.map(|field| InputFieldDefinition {
name: field.name.clone(),
description: field.description.clone(),
default_value: None,
default_value: field.default_value.clone(),
of_type: field.of_type.clone(),
})
.collect(),
Expand Down Expand Up @@ -279,6 +279,7 @@ fn update_args<'a>(
of_type: to_type(*field, None),
directives: Vec::new(),
resolver: None,
default_value: field.default_value.clone(),
})
},
)
Expand Down
40 changes: 37 additions & 3 deletions src/core/blueprint/into_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,29 @@ fn to_type_ref(type_of: &Type) -> dynamic::TypeRef {
}
}

/// We set the default value for an `InputValue` by reading it from the
/// blueprint and assigning it to the provided `InputValue` during the
/// generation of the `async_graphql::Schema`. The `InputValue` represents the
/// structure of arguments and their types that can be passed to a field. In
/// other GraphQL implementations, this is commonly referred to as
/// `InputValueDefinition`.
fn set_default_value(
input_value: dynamic::InputValue,
value: Option<serde_json::Value>,
) -> dynamic::InputValue {
if let Some(value) = value {
match ConstValue::from_json(value) {
Ok(const_value) => input_value.default_value(const_value),
Err(err) => {
tracing::warn!("conversion from serde_json::Value to ConstValue failed for default_value with error {err:?}");
input_value
}
}
} else {
input_value
}
}

fn to_type(def: &Definition) -> dynamic::Type {
match def {
Definition::Object(def) => {
Expand All @@ -46,6 +69,11 @@ fn to_type(def: &Definition) -> dynamic::Type {
field_name,
type_ref.clone(),
move |ctx| {
// region: HOT CODE
// --------------------------------------------------
// HOT CODE STARTS HERE
// --------------------------------------------------

let req_ctx = ctx.ctx.data::<Arc<RequestContext>>().unwrap();
let field_name = &field.name;

Expand Down Expand Up @@ -82,15 +110,20 @@ fn to_type(def: &Definition) -> dynamic::Type {
)
}
}

// --------------------------------------------------
// HOT CODE ENDS HERE
// --------------------------------------------------
// endregion: hot_code
},
);
if let Some(description) = &field.description {
dyn_schema_field = dyn_schema_field.description(description);
}
for arg in field.args.iter() {
dyn_schema_field = dyn_schema_field.argument(dynamic::InputValue::new(
arg.name.clone(),
to_type_ref(&arg.of_type),
dyn_schema_field = dyn_schema_field.argument(set_default_value(
dynamic::InputValue::new(arg.name.clone(), to_type_ref(&arg.of_type)),
arg.default_value.clone(),
));
}
object = object.field(dyn_schema_field);
Expand Down Expand Up @@ -123,6 +156,7 @@ fn to_type(def: &Definition) -> dynamic::Type {
if let Some(description) = &field.description {
input_field = input_field.description(description);
}
let input_field = set_default_value(input_field, field.default_value.clone());
input_object = input_object.field(input_field);
}
if let Some(description) = &def.description {
Expand Down
6 changes: 6 additions & 0 deletions src/core/config/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,16 @@ pub struct Field {
///
/// Sets the cache configuration for a field
pub cache: Option<Cache>,

///
/// Marks field as protected by auth provider
#[serde(default)]
pub protected: Option<Protected>,

///
/// Stores the default value for the field
#[serde(default, skip_serializing_if = "is_default")]
pub default_value: Option<Value>,
}

// It's a terminal implementation of MergeRight
Expand Down
32 changes: 24 additions & 8 deletions src/core/config/from_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use async_graphql::parser::types::{
};
use async_graphql::parser::Positioned;
use async_graphql::Name;
use async_graphql_value::ConstValue;

use super::telemetry::Telemetry;
use super::{Tag, JS};
Expand All @@ -15,7 +16,7 @@ use crate::core::config::{
Server, Union, Upstream,
};
use crate::core::directive::DirectiveCodec;
use crate::core::valid::{Valid, Validator};
use crate::core::valid::{Valid, ValidationError, Validator};

const DEFAULT_SCHEMA_DEFINITION: &SchemaDefinition = &SchemaDefinition {
extend: false,
Expand Down Expand Up @@ -291,23 +292,36 @@ fn to_input_object_fields(
to_fields_inner(input_object_fields, to_input_object_field)
}
fn to_field(field_definition: &FieldDefinition) -> Valid<config::Field, String> {
to_common_field(field_definition, to_args(field_definition))
to_common_field(field_definition, to_args(field_definition), None)
}
fn to_input_object_field(field_definition: &InputValueDefinition) -> Valid<config::Field, String> {
to_common_field(field_definition, BTreeMap::new())
to_common_field(
field_definition,
BTreeMap::new(),
field_definition
.default_value
.as_ref()
.map(|f| f.node.clone()),
)
}
fn to_common_field<F>(
field: &F,
args: BTreeMap<String, config::Arg>,
default_value: Option<ConstValue>,
) -> Valid<config::Field, String>
where
F: Fieldlike,
F: FieldLike,
{
let type_of = field.type_of();
let base = &type_of.base;
let nullable = &type_of.nullable;
let description = field.description();
let directives = field.directives();
let default_value = default_value
.map(ConstValue::into_json)
.transpose()
.map_err(|err| ValidationError::new(err.to_string()))
.into();

let type_of = to_type_of(type_of);
let list = matches!(&base, BaseType::List(_));
Expand All @@ -322,8 +336,9 @@ where
.fuse(JS::from_directives(directives.iter()))
.fuse(Call::from_directives(directives.iter()))
.fuse(Protected::from_directives(directives.iter()))
.fuse(default_value)
.map(
|(http, graphql, cache, grpc, omit, modify, script, call, protected)| {
|(http, graphql, cache, grpc, omit, modify, script, call, protected, default_value)| {
let const_field = to_const_field(directives);
config::Field {
type_of,
Expand All @@ -342,6 +357,7 @@ where
cache,
call,
protected,
default_value,
}
},
)
Expand Down Expand Up @@ -445,12 +461,12 @@ impl HasName for InputValueDefinition {
}
}

trait Fieldlike {
trait FieldLike {
fn type_of(&self) -> &Type;
fn description(&self) -> &Option<Positioned<String>>;
fn directives(&self) -> &[Positioned<ConstDirective>];
}
impl Fieldlike for FieldDefinition {
impl FieldLike for FieldDefinition {
fn type_of(&self) -> &Type {
&self.ty.node
}
Expand All @@ -461,7 +477,7 @@ impl Fieldlike for FieldDefinition {
&self.directives
}
}
impl Fieldlike for InputValueDefinition {
impl FieldLike for InputValueDefinition {
fn type_of(&self) -> &Type {
&self.ty.node
}
Expand Down
16 changes: 11 additions & 5 deletions src/core/config/into_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ use crate::core::directive::DirectiveCodec;
fn pos<A>(a: A) -> Positioned<A> {
Positioned::new(a, Pos::default())
}

fn transform_default_value(value: Option<serde_json::Value>) -> Option<ConstValue> {
value.map(ConstValue::from_json).and_then(Result::ok)
}

fn config_document(config: &ConfigModule) -> ServiceDocument {
let mut definitions = Vec::new();
let mut directives = vec![
Expand Down Expand Up @@ -112,7 +117,8 @@ fn config_document(config: &ConfigModule) -> ServiceDocument {
name: pos(Name::new(name.clone())),
ty: pos(Type { nullable: !field.required, base: base_type }),

default_value: None,
default_value: transform_default_value(field.default_value.clone())
.map(pos),
directives,
})
})
Expand Down Expand Up @@ -167,10 +173,10 @@ fn config_document(config: &ConfigModule) -> ServiceDocument {
name: pos(Name::new(name.clone())),
ty: pos(Type { nullable: !arg.required, base: base_type }),

default_value: arg
.default_value
.clone()
.map(|v| pos(ConstValue::String(v.to_string()))),
default_value: transform_default_value(
arg.default_value.clone(),
)
.map(pos),
directives: Vec::new(),
})
})
Expand Down
23 changes: 20 additions & 3 deletions src/core/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,13 @@ fn print_field(field: &async_graphql::parser::types::FieldDefinition) -> String
.iter()
.map(|arg| {
let nullable = if arg.node.ty.node.nullable { "" } else { "!" };
format!("{}: {}{}", arg.node.name, arg.node.ty.node.base, nullable)
format!(
"{}: {}{}{}",
arg.node.name,
arg.node.ty.node.base,
nullable,
print_default_value(arg.node.default_value.as_ref())
)
})
.collect::<Vec<String>>()
.join(", ");
Expand All @@ -215,14 +221,25 @@ fn print_field(field: &async_graphql::parser::types::FieldDefinition) -> String
doc + node.trim_end()
}

fn print_default_value(value: Option<&Positioned<ConstValue>>) -> String {
value
.as_ref()
.map(|val| format!(" = {val}"))
.unwrap_or_default()
}

fn print_input_value(field: &async_graphql::parser::types::InputValueDefinition) -> String {
let directives_str = print_directives(&field.directives);
let doc = field.description.as_ref().map_or(String::new(), |d| {
format!(r#" """{} {}{} """{}"#, "\n", d.node, "\n", "\n")
});
format!(
"{} {}: {}{}",
doc, field.name.node, field.ty.node, directives_str
"{} {}: {}{}{}",
doc,
field.name.node,
field.ty.node,
directives_str,
print_default_value(field.default_value.as_ref())
)
}
fn print_directive(directive: &DirectiveDefinition) -> String {
Expand Down
15 changes: 15 additions & 0 deletions tests/core/snapshots/default-value-arg.md_0.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
source: tests/core/spec.rs
expression: response
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"data": {
"bar": 1
}
}
}
15 changes: 15 additions & 0 deletions tests/core/snapshots/default-value-arg.md_1.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
source: tests/core/spec.rs
expression: response
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"data": {
"bar": 2
}
}
}
49 changes: 49 additions & 0 deletions tests/core/snapshots/default-value-arg.md_client.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
source: tests/core/spec.rs
expression: formatted
---
scalar Bytes

scalar Date

scalar Email

scalar Empty

input Input {
id: Int!
}

scalar Int128

scalar Int16

scalar Int32

scalar Int64

scalar Int8

scalar JSON

scalar PhoneNumber

type Query {
bar(input: Input = {id: 1}): Int
}

scalar UInt128

scalar UInt16

scalar UInt32

scalar UInt64

scalar UInt8

scalar Url

schema {
query: Query
}
Loading

1 comment on commit c4dfd8b

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running 30s test @ http://localhost:8000/graphql

4 threads and 100 connections

Thread Stats Avg Stdev Max +/- Stdev
Latency 6.68ms 2.95ms 81.83ms 72.42%
Req/Sec 3.78k 207.45 4.11k 93.67%

451711 requests in 30.00s, 2.26GB read

Requests/sec: 15055.09

Transfer/sec: 77.27MB

Please sign in to comment.