From f090df76f716d4764cfbdbb6390703d22a4673cd Mon Sep 17 00:00:00 2001 From: sunli Date: Mon, 2 Mar 2020 19:25:21 +0800 Subject: [PATCH] add schema model --- async-graphql-derive/Cargo.toml | 1 + async-graphql-derive/src/args.rs | 210 ++++++++++++----------- async-graphql-derive/src/enum.rs | 2 +- async-graphql-derive/src/input_object.rs | 3 + async-graphql-derive/src/object.rs | 125 +++++--------- examples/helloworld.rs | 115 +++---------- src/schema/enum_value.rs | 31 ++++ src/schema/field.rs | 45 +++-- src/schema/input_value.rs | 36 ++-- src/schema/kind.rs | 5 +- src/schema/mod.rs | 2 + src/schema/type.rs | 92 ++++++---- src/types/non_null.rs | 9 +- 13 files changed, 339 insertions(+), 337 deletions(-) create mode 100644 src/schema/enum_value.rs diff --git a/async-graphql-derive/Cargo.toml b/async-graphql-derive/Cargo.toml index 66208dd5..a2069415 100644 --- a/async-graphql-derive/Cargo.toml +++ b/async-graphql-derive/Cargo.toml @@ -19,3 +19,4 @@ proc-macro = true proc-macro2 = "1.0.6" syn = { version = "1.0.13", features = ["full"] } quote = "1.0.2" +Inflector = "0.11.4" diff --git a/async-graphql-derive/src/args.rs b/async-graphql-derive/src/args.rs index 81745457..b150b5c5 100644 --- a/async-graphql-derive/src/args.rs +++ b/async-graphql-derive/src/args.rs @@ -1,28 +1,25 @@ -use syn::{Attribute, AttributeArgs, Error, Meta, NestedMeta, Result, Type}; +use syn::{Attribute, AttributeArgs, Error, Meta, MetaList, NestedMeta, Result, Type}; #[derive(Debug)] pub struct Object { pub internal: bool, - pub auto_impl: bool, pub name: Option, pub desc: Option, + pub fields: Vec, } impl Object { pub fn parse(args: AttributeArgs) -> Result { let mut internal = false; - let mut auto_impl = false; let mut name = None; let mut desc = None; + let mut fields = Vec::new(); for arg in args { match arg { NestedMeta::Meta(Meta::Path(p)) if p.is_ident("internal") => { internal = true; } - NestedMeta::Meta(Meta::Path(p)) if p.is_ident("auto_impl") => { - auto_impl = true; - } NestedMeta::Meta(Meta::NameValue(nv)) => { if nv.path.is_ident("name") { if let syn::Lit::Str(lit) = nv.lit { @@ -44,15 +41,18 @@ impl Object { } } } + NestedMeta::Meta(Meta::List(ls)) if ls.path.is_ident("field") => { + fields.push(Field::parse(&ls)?); + } _ => {} } } Ok(Self { internal, - auto_impl, name, desc, + fields, }) } } @@ -66,34 +66,82 @@ pub struct Argument { #[derive(Debug)] pub struct Field { - pub name: Option, + pub name: String, + pub resolver: Option, pub desc: Option, - pub is_attr: bool, - pub attr_type: Option, + pub ty: Type, + pub is_owned: bool, pub arguments: Vec, } impl Field { - pub fn parse(attrs: &[Attribute]) -> Result> { - let mut is_field = false; + fn parse(ls: &MetaList) -> Result { let mut name = None; + let mut resolver = None; let mut desc = None; - let mut is_attr = false; - let mut attr_type = None; + let mut ty = None; + let mut is_owned = false; let mut arguments = Vec::new(); - for attr in attrs { - if attr.path.is_ident("field") { - is_field = true; - if let Ok(Meta::List(args)) = attr.parse_meta() { - for meta in args.nested { - match meta { - NestedMeta::Meta(Meta::Path(p)) if p.is_ident("attr") => { - is_attr = true; + for meta in &ls.nested { + match meta { + NestedMeta::Meta(Meta::Path(p)) if p.is_ident("owned") => { + is_owned = true; + } + NestedMeta::Meta(Meta::NameValue(nv)) => { + if nv.path.is_ident("name") { + if let syn::Lit::Str(lit) = &nv.lit { + name = Some(lit.value()); + } else { + return Err(Error::new_spanned( + &nv.lit, + "Attribute 'name' should be a string.", + )); + } + } else if nv.path.is_ident("desc") { + if let syn::Lit::Str(lit) = &nv.lit { + desc = Some(lit.value()); + } else { + return Err(Error::new_spanned( + &nv.lit, + "Attribute 'desc' should be a string.", + )); + } + } else if nv.path.is_ident("resolver") { + if let syn::Lit::Str(lit) = &nv.lit { + resolver = Some(lit.value()); + } else { + return Err(Error::new_spanned( + &nv.lit, + "Attribute 'resolver' should be a string.", + )); + } + } else if nv.path.is_ident("type") { + if let syn::Lit::Str(lit) = &nv.lit { + if let Ok(ty2) = syn::parse_str::(&lit.value()) { + ty = Some(ty2); + } else { + return Err(Error::new_spanned(&lit, "Expect type")); } - NestedMeta::Meta(Meta::NameValue(nv)) => { + desc = Some(lit.value()); + } else { + return Err(Error::new_spanned( + &nv.lit, + "Attribute 'type' should be a string.", + )); + } + } + } + NestedMeta::Meta(Meta::List(ls)) => { + if ls.path.is_ident("arg") { + let mut name = None; + let mut desc = None; + let mut ty = None; + + for meta in &ls.nested { + if let NestedMeta::Meta(Meta::NameValue(nv)) = meta { if nv.path.is_ident("name") { - if let syn::Lit::Str(lit) = nv.lit { + if let syn::Lit::Str(lit) = &nv.lit { name = Some(lit.value()); } else { return Err(Error::new_spanned( @@ -102,7 +150,7 @@ impl Field { )); } } else if nv.path.is_ident("desc") { - if let syn::Lit::Str(lit) = nv.lit { + if let syn::Lit::Str(lit) = &nv.lit { desc = Some(lit.value()); } else { return Err(Error::new_spanned( @@ -110,102 +158,58 @@ impl Field { "Attribute 'desc' should be a string.", )); } - } else if nv.path.is_ident("attr_type") { + } else if nv.path.is_ident("type") { if let syn::Lit::Str(lit) = &nv.lit { - if let Ok(ty) = syn::parse_str::(&lit.value()) { - attr_type = Some(ty); + if let Ok(ty2) = syn::parse_str::(&lit.value()) { + ty = Some(ty2); } else { return Err(Error::new_spanned(&lit, "expect type")); } } else { return Err(Error::new_spanned( &nv.lit, - "Attribute 'attr_type' should be a string.", + "Attribute 'type' should be a string.", )); } } } - NestedMeta::Meta(Meta::List(ls)) => { - if ls.path.is_ident("arg") { - let mut name = None; - let mut desc = None; - let mut ty = None; - - for meta in &ls.nested { - if let NestedMeta::Meta(Meta::NameValue(nv)) = meta { - if nv.path.is_ident("name") { - if let syn::Lit::Str(lit) = &nv.lit { - name = Some(lit.value()); - } else { - return Err(Error::new_spanned( - &nv.lit, - "Attribute 'name' should be a string.", - )); - } - } else if nv.path.is_ident("desc") { - if let syn::Lit::Str(lit) = &nv.lit { - desc = Some(lit.value()); - } else { - return Err(Error::new_spanned( - &nv.lit, - "Attribute 'desc' should be a string.", - )); - } - } else if nv.path.is_ident("type") { - if let syn::Lit::Str(lit) = &nv.lit { - if let Ok(ty2) = - syn::parse_str::(&lit.value()) - { - ty = Some(ty2); - } else { - return Err(Error::new_spanned( - &lit, - "expect type", - )); - } - } else { - return Err(Error::new_spanned( - &nv.lit, - "Attribute 'type' should be a string.", - )); - } - } - } - } - - if name.is_none() { - return Err(Error::new_spanned(ls, "missing name.")); - } - - if ty.is_none() { - return Err(Error::new_spanned(ls, "missing type.")); - } - - arguments.push(Argument { - name: name.unwrap(), - desc, - ty: ty.unwrap(), - }); - } - } - _ => {} } + + if name.is_none() { + return Err(Error::new_spanned(ls, "missing name.")); + } + + if ty.is_none() { + return Err(Error::new_spanned(ls, "missing type.")); + } + + arguments.push(Argument { + name: name.unwrap(), + desc, + ty: ty.unwrap(), + }); } } + _ => {} } } - if is_field { - Ok(Some(Self { - name, - desc, - is_attr, - attr_type, - arguments, - })) - } else { - Ok(None) + if name.is_none() { + return Err(Error::new_spanned(ls, "missing name.")); } + + if ty.is_none() { + return Err(Error::new_spanned(ls, "missing type.")); + } + + Ok(Self { + name: name.unwrap(), + resolver, + desc, + ty: ty.unwrap(), + is_owned, + arguments, + }) } } diff --git a/async-graphql-derive/src/enum.rs b/async-graphql-derive/src/enum.rs index 23c2461a..095f624e 100644 --- a/async-graphql-derive/src/enum.rs +++ b/async-graphql-derive/src/enum.rs @@ -81,7 +81,7 @@ pub fn generate(enum_args: &args::Enum, input: &DeriveInput) -> Result) -> #crate_name::Result { - self.resolve_enum() + #crate_name::GQLEnum::resolve_enum(self) } } }; diff --git a/async-graphql-derive/src/input_object.rs b/async-graphql-derive/src/input_object.rs index a88097ad..94a2cfbc 100644 --- a/async-graphql-derive/src/input_object.rs +++ b/async-graphql-derive/src/input_object.rs @@ -46,6 +46,8 @@ pub fn generate(object_args: &args::Object, input: &DeriveInput) -> Result #crate_name::Result { + use #crate_name::GQLType; + if let #crate_name::Value::Object(mut obj) = value { #(#get_fields)* Ok(Self { #(#fields),* }) @@ -58,6 +60,7 @@ pub fn generate(object_args: &args::Object, input: &DeriveInput) -> Result #crate_name::Result { + use #crate_name::GQLType; if let #crate_name::serde_json::Value::Object(mut obj) = value { #(#get_json_fields)* Ok(Self { #(#fields),* }) diff --git a/async-graphql-derive/src/object.rs b/async-graphql-derive/src/object.rs index 31452f79..4a8ea523 100644 --- a/async-graphql-derive/src/object.rs +++ b/async-graphql-derive/src/object.rs @@ -1,5 +1,6 @@ use crate::args; use crate::utils::get_crate_name; +use inflector::Inflector; use proc_macro::TokenStream; use proc_macro2::Span; use quote::quote; @@ -7,11 +8,10 @@ use syn::{Data, DeriveInput, Error, Ident, Result}; pub fn generate(object_args: &args::Object, input: &DeriveInput) -> Result { let crate_name = get_crate_name(object_args.internal); - let attrs = &input.attrs; let vis = &input.vis; let ident = &input.ident; - let s = match &input.data { - Data::Struct(s) => s, + match &input.data { + Data::Struct(_) => {} _ => return Err(Error::new_spanned(input, "It should be a struct.")), }; @@ -20,94 +20,63 @@ pub fn generate(object_args: &args::Object, input: &DeriveInput) -> Result, #(#decl_params),*) -> #crate_name::Result<#ty>; - }); - - resolvers.push(quote! { - if field.name.as_str() == #field_name { - #(#get_params)* - let obj = #trait_ident::#ident(self, &ctx_field, #(#use_params),*).await. - map_err(|err| err.with_position(field.position))?; - let ctx_obj = ctx_field.with_item(&field.selection_set); - let value = obj.resolve(&ctx_obj).await. - map_err(|err| err.with_position(field.position))?; - let name = field.alias.clone().unwrap_or_else(|| field.name.clone()); - result.insert(name, value.into()); - continue; - } - }); + async fn #resolver(&self, ctx: &#crate_name::ContextField<'_>, #(#decl_params),*) -> #crate_name::Result<#ty>; + }); } else { - obj_attrs.push(quote! { #field }); + trait_fns.push(quote! { + async fn #resolver<'a>(&'a self, ctx: &#crate_name::ContextField<'_>, #(#decl_params),*) -> #crate_name::Result<&'a #ty>; + }); } - } - let mut impl_fields = quote! {}; - if object_args.auto_impl && all_is_simple_attr { - let mut impl_fns = Vec::new(); - for field in obj_fields { - let ident = &field.ident; - let ty = &field.ty; - impl_fns.push(quote! { - async fn #ident(&self, _: &#crate_name::ContextField<'_>) -> #crate_name::Result<#ty> { - Ok(self.#ident.clone()) - } - }); - } - impl_fields = quote! { - #[#crate_name::async_trait::async_trait] - impl #trait_ident for #ident { - #(#impl_fns)* + resolvers.push(quote! { + if field.name.as_str() == #field_name { + #(#get_params)* + let obj = #trait_ident::#resolver(self, &ctx_field, #(#use_params),*).await. + map_err(|err| err.with_position(field.position))?; + let ctx_obj = ctx_field.with_item(&field.selection_set); + let value = obj.resolve(&ctx_obj).await. + map_err(|err| err.with_position(field.position))?; + let name = field.alias.clone().unwrap_or_else(|| field.name.clone()); + result.insert(name, value.into()); + continue; } - }; + }); } let expanded = quote! { - #(#attrs)* - #vis struct #ident { - #(#obj_attrs),* - } + #input #[#crate_name::async_trait::async_trait] #vis trait #trait_ident { @@ -156,8 +125,6 @@ pub fn generate(object_args: &args::Object, input: &DeriveInput) -> Result, a1: i32, a2: f32) -> Result { - Ok(a1 + a2 as i32) + async fn a<'a>( + &'a self, + ctx: &async_graphql::ContextField<'_>, + ) -> async_graphql::Result<&'a i32> { + Ok(&self.value) } - async fn b(&self, ctx: &ContextField<'_>) -> Result { + async fn b(&self, ctx: &async_graphql::ContextField<'_>) -> async_graphql::Result { Ok(999) } - async fn c(&self, ctx: &ContextField<'_>, input: MyEnum) -> Result { - Ok(input) - } - - async fn d(&self, ctx: &ContextField<'_>, input: MyInputObj) -> Result { - Ok(input.a + input.b) - } - - async fn e(&self, ctx: &ContextField<'_>) -> Result<&'static str> { - Ok("hehehe") - } - - async fn f(&self, ctx: &ContextField<'_>) -> Result<&'static [i32]> { - Ok(&[1, 2, 3, 4, 5]) - } - - async fn g(&self, ctx: &ContextField<'_>) -> Result<&'static MyObj2> { - Ok(&MyObj2 { a: 10, b: 20 }) - } - - async fn child(&self, ctx: &ContextField<'_>) -> async_graphql::Result { - Ok(ChildObj { value: 10.0 }) - } -} - -#[async_trait::async_trait] -impl ChildObjFields for ChildObj { - async fn value(&self, ctx: &ContextField<'_>) -> Result { - Ok(self.value) + async fn c( + &self, + ctx: &async_graphql::ContextField<'_>, + ) -> async_graphql::Result> { + Ok(Some(format!("**{}**", self.value))) } } #[async_std::main] async fn main() { - let res = QueryBuilder::new( - MyObj { a: 10 }, - GQLEmptyMutation, - "{ b c(input:B) d(input:{a:10 b:20}) e f g { a b } }", + let res = async_graphql::QueryBuilder::new( + MyObj { value: 100 }, + async_graphql::GQLEmptyMutation, + "{ a b c }", ) .execute() .await diff --git a/src/schema/enum_value.rs b/src/schema/enum_value.rs new file mode 100644 index 00000000..671bd571 --- /dev/null +++ b/src/schema/enum_value.rs @@ -0,0 +1,31 @@ +use crate::{ContextField, Result}; +use async_graphql_derive::Object; + +#[Object( + internal, + desc = "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + field(name = "name", type = "String", owned), + field(name = "description", type = "Option", owned), + field(name = "isDeprecated", type = "bool", owned), + field(name = "deprecationReason", type = "Option", owned) +)] +pub struct __EnumValue {} + +#[async_trait::async_trait] +impl __EnumValueFields for __EnumValue { + async fn name(&self, _: &ContextField<'_>) -> Result { + todo!() + } + + async fn description(&self, _: &ContextField<'_>) -> Result> { + todo!() + } + + async fn is_deprecated(&self, _: &ContextField<'_>) -> Result { + todo!() + } + + async fn deprecation_reason(&self, _: &ContextField<'_>) -> Result> { + todo!() + } +} diff --git a/src/schema/field.rs b/src/schema/field.rs index afd0b47e..e57719ad 100644 --- a/src/schema/field.rs +++ b/src/schema/field.rs @@ -1,23 +1,38 @@ -use crate::schema::{__InputValue, __Type}; +use crate::schema::__Type; +use crate::{ContextField, Result}; use async_graphql_derive::Object; -#[Object(internal, auto_impl)] -pub struct __Field { - #[field(attr)] - name: &'static str, +#[Object( + internal, + desc = "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + field(name = "name", type = "String", owned), + field(name = "description", type = "Option", owned), + field(name = "type", resolver = "ty", type = "__Type", owned), + field(name = "isDeprecated", type = "bool", owned), + field(name = "deprecationReason", type = "Option", owned) +)] +pub struct __Field {} - #[field(attr)] - description: Option<&'static str>, +#[async_trait::async_trait] +#[allow()] +impl __FieldFields for __Field { + async fn name(&self, _: &ContextField<'_>) -> Result { + todo!() + } - #[field(attr)] - args: &'static [__InputValue], + async fn description(&self, _: &ContextField<'_>) -> Result> { + todo!() + } - #[field(attr, name = "type")] - ty: &'static __Type, + async fn ty(&self, _: &ContextField<'_>) -> Result<__Type> { + todo!() + } - #[field(attr, name = "isDeprecated")] - is_deprecated: bool, + async fn is_deprecated(&self, _: &ContextField<'_>) -> Result { + todo!() + } - #[field(attr, name = "deprecationReason")] - deprecation_reason: Option, + async fn deprecation_reason(&self, _: &ContextField<'_>) -> Result> { + todo!() + } } diff --git a/src/schema/input_value.rs b/src/schema/input_value.rs index 28166807..4d83c947 100644 --- a/src/schema/input_value.rs +++ b/src/schema/input_value.rs @@ -1,18 +1,32 @@ use crate::schema::__Type; -use crate::Value; +use crate::{ContextField, Result}; use async_graphql_derive::Object; -#[Object(internal)] -pub struct __InputValue { - #[field(attr)] - name: &'static str, +#[Object( + internal, + desc = "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + field(name = "name", type = "String", owned), + field(name = "description", type = "Option", owned), + field(name = "type", resolver = "ty", type = "__Type", owned), + field(name = "defaultValue", type = "String", owned) +)] +pub struct __InputValue {} - #[field(attr)] - description: Option<&'static str>, +#[async_trait::async_trait] +impl __InputValueFields for __InputValue { + async fn name(&self, _: &ContextField<'_>) -> Result { + todo!() + } - #[field(attr, name = "type")] - ty: &'static __Type, + async fn description(&self, _: &ContextField<'_>) -> Result> { + todo!() + } - #[field(attr, attr_type = "Value", name = "defaultValue")] - default_value: String, + async fn ty(&self, _: &ContextField<'_>) -> Result<__Type> { + todo!() + } + + async fn default_value(&self, _: &ContextField<'_>) -> Result { + todo!() + } } diff --git a/src/schema/kind.rs b/src/schema/kind.rs index c599f74a..bc86ddf8 100644 --- a/src/schema/kind.rs +++ b/src/schema/kind.rs @@ -1,6 +1,9 @@ use async_graphql_derive::Enum; -#[Enum(internal)] +#[Enum( + internal, + desc = "An enum describing what kind of type a given `__Type` is." +)] #[allow(non_camel_case_types)] pub enum __TypeKind { #[item(desc = "Indicates this type is a scalar.")] diff --git a/src/schema/mod.rs b/src/schema/mod.rs index dbc4c2a3..6710c9a5 100644 --- a/src/schema/mod.rs +++ b/src/schema/mod.rs @@ -1,8 +1,10 @@ +mod enum_value; mod field; mod input_value; mod kind; mod r#type; +pub use enum_value::__EnumValue; pub use field::__Field; pub use input_value::__InputValue; pub use kind::__TypeKind; diff --git a/src/schema/type.rs b/src/schema/type.rs index 2b32bd65..0e3b148c 100644 --- a/src/schema/type.rs +++ b/src/schema/type.rs @@ -1,36 +1,70 @@ -use crate::schema::{__Field, __InputValue, __TypeKind}; +use crate::schema::{__EnumValue, __InputValue, __TypeKind}; +use crate::{ContextField, Result}; use async_graphql_derive::Object; -#[Object(internal)] -pub struct __Type { - #[field(attr)] - kind: __TypeKind, +#[Object( + internal, + desc = r#" +The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum. - #[field(attr)] - name: Option<&'static str>, - - #[field(attr)] - description: Option<&'static str>, - - #[field(attr, arg(name = "includeDeprecated", type = "bool"))] - fields: Option<&'static [__Field]>, - - #[field(attr)] - interfaces: Option<&'static [__Type]>, - - #[field(attr, name = "possibleTypes")] - possible_types: Option<&'static [__Type]>, - - #[field( - attr, - name = "enumValues", +Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types. +"#, + field(name = "kind", type = "__TypeKind", owned), + field(name = "name", type = "String", owned), + field(name = "description", type = "Option", owned), + field( + name = "fields", + type = "Option>", + owned, arg(name = "includeDeprecated", type = "bool") - )] - enum_values: Option<&'static [__Type]>, + ), + field(name = "interfaces", type = "Option>", owned), + field(name = "possibleTypes", type = "Option>", owned), + field(name = "enumValues", type = "Option>", owned), + field(name = "inputFields", type = "Option>", owned), + field(name = "ofType", type = "Option<__Type>", owned) +)] +pub struct __Type {} - #[field(attr, name = "inputFields")] - input_fields: Option<&'static [__InputValue]>, +#[async_trait::async_trait] +impl __TypeFields for __Type { + async fn kind<'a>(&'a self, _: &ContextField<'_>) -> Result<__TypeKind> { + todo!() + } - #[field(attr, name = "ofType")] - of_type: Option<&'static __Type>, + async fn name<'a>(&'a self, _: &ContextField<'_>) -> Result { + todo!() + } + + async fn description<'a>(&'a self, _: &ContextField<'_>) -> Result> { + todo!() + } + + async fn fields<'a>( + &'a self, + _: &ContextField<'_>, + include_deprecated: bool, + ) -> Result>> { + todo!() + } + + async fn interfaces<'a>(&'a self, _: &ContextField<'_>) -> Result>> { + todo!() + } + + async fn possible_types<'a>(&'a self, _: &ContextField<'_>) -> Result>> { + todo!() + } + + async fn enum_values<'a>(&'a self, _: &ContextField<'_>) -> Result>> { + todo!() + } + + async fn input_fields<'a>(&'a self, _: &ContextField<'_>) -> Result>> { + todo!() + } + + async fn of_type<'a>(&'a self, _: &ContextField<'_>) -> Result> { + todo!() + } } diff --git a/src/types/non_null.rs b/src/types/non_null.rs index ff63c353..80199e25 100644 --- a/src/types/non_null.rs +++ b/src/types/non_null.rs @@ -3,7 +3,8 @@ use std::borrow::Cow; impl GQLType for Option { fn type_name() -> Cow<'static, str> { - Cow::Owned(format!("{}", T::type_name().trim_end_matches("!"))) + let name = T::type_name(); + Cow::Owned(format!("{}", &name[..name.len() - 1])) } } @@ -24,12 +25,12 @@ impl GQLInputValue for Option { } #[async_trait::async_trait] -impl GQLOutputValue for Option { - async fn resolve(&self, ctx: &ContextSelectionSet<'_>) -> Result { +impl GQLOutputValue for Option { + async fn resolve(&self, ctx: &ContextSelectionSet<'_>) -> Result where { if let Some(inner) = self { inner.resolve(ctx).await } else { Ok(serde_json::Value::Null) } } -} +} \ No newline at end of file