//! Models and custom GraphQL endpoints.
use crate::engine::schema::{rel_name_variants, type_name_variants};
use crate::Error;
use log::trace;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::fs::File;
use std::io::BufReader;
use std::slice::Iter;
const LATEST_CONFIG_VERSION: i32 = 2;
// Convenience function for setting serde default value
fn get_false() -> bool {
false
}
// Convenience function for setting serde default value
fn get_true() -> bool {
true
}
// Convenience function for setting serde default value
fn get_none() -> Option<String> {
None
}
/// Configuration for a Warpgrapher data model. The configuration contains the version of the
/// Warpgrapher configuration file format, a vector of [`Type`] structures, and a vector of
/// [`Endpoint`] structures.
///
/// [`Endpoint`]: struct.Endpoint.html
/// [`Type`]: struct.Type.html
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::Configuration;
///
/// let c = Configuration::new(1, Vec::new(), Vec::new());
/// ```
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Configuration {
/// Version of the Warpgrapher configuration file format used
version: i32,
/// A vector of [`Type`] structures, each defining one type in the data model
///
/// [`Type`]: struct.Type.html
#[serde(default)]
pub model: Vec<Type>,
/// A vector of [`Endpoint`] structures, each defining a custom root endpoint in the graphql
/// schema
///
/// [`Endpoint`]: struct.Endpoint.html
#[serde(default)]
endpoints: Vec<Endpoint>,
}
impl Configuration {
/// Creates a new [`Configuration`] data structure with the version, [`Type`] vector, and
/// [`Endpoint`] vector provided as arguments.
///
/// [`Configuration`]: struct.Configuration.html
/// [`Endpoint`]: struct.Endpoint.html
/// [`Type`]: struct.Type.html
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::Configuration;
///
/// let c = Configuration::new(1, Vec::new(), Vec::new());
/// ```
pub fn new(version: i32, model: Vec<Type>, endpoints: Vec<Endpoint>) -> Configuration {
Configuration {
version,
model,
endpoints,
}
}
/// Returns an iterator over the [`Endpoint`] structs defining custom root endpoints in the
/// GraphQL schema
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::Configuration;
///
/// let c = Configuration::new(1, Vec::new(), Vec::new());
/// for e in c.endpoints() {
/// let _name = e.name();
/// }
/// ```
pub fn endpoints(&self) -> Iter<Endpoint> {
self.endpoints.iter()
}
/// Returns an iterator over the [`Type`] structs defining types in the GraphQL schema
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::Configuration;
///
/// let c = Configuration::new(1, Vec::new(), Vec::new());
/// for t in c.types() {
/// let _name = t.name();
/// }
/// ```
pub fn types(&self) -> Iter<Type> {
self.model.iter()
}
/// Validates the [`Configuration`] data structure. Checks that there are no duplicate
/// [`Endpoint`] or [`Type`] items, and that the [`Endpoint`] input/output types are defined
/// in the model. Returns () if there are no validation errors.
///
/// # Errors
///
/// Returns an [`Error`] variant [`ConfigItemDuplicated`] if there is more than one type or
/// more than one endpoint that use the same name.
///
/// Returns an [`Error`] variant [`ConfigItemReserved`] if a named configuration item, such as
/// an endpoint or type, has a name that is a reserved word, such as "ID" or the name of a
/// GraphQL scalar type.
///
/// [`ConfigItemDuplicated`]: ../../error/enum.Error.html#variant.ConfigItemDuplicated
/// [`ConfigItemReserved`]: ../../error/enum.Error.html#variant.ConfigItemReserved
/// [`Error`]: ../../error/enum.Error.html
///
/// # Example
/// ```rust
/// # use warpgrapher::Configuration;
///
/// let config = Configuration::new(1, Vec::new(), Vec::new());
/// config.validate();
/// ```
pub fn validate(&self) -> Result<(), Error> {
trace!("Config::validate called");
let scalar_names = ["Int", "Float", "Boolean", "String", "ID"];
self.model
.iter()
.map(|t| {
if self.model.iter().filter(|t2| t2.name == t.name).count() > 1 {
return Err(Error::ConfigItemDuplicated {
type_name: t.name.to_string(),
});
}
let name_variants = type_name_variants(t);
self.model.iter().try_for_each(|t2| {
if name_variants.contains(t2.name()) {
Err(Error::ConfigItemDuplicated {
type_name: t2.name().to_string(),
})
} else {
Ok(())
}
})?;
if scalar_names.iter().any(|s| s == &t.name) {
return Err(Error::ConfigItemReserved {
type_name: t.name.clone(),
});
}
if t.props.iter().any(|p| p.name().to_uppercase() == "ID") {
return Err(Error::ConfigItemReserved {
type_name: "ID".to_string(),
});
}
// Used by Gremlin return format to return the label itself
if t.props.iter().any(|p| p.name().to_uppercase() == "LABEL") {
return Err(Error::ConfigItemReserved {
type_name: "label".to_string(),
});
}
if t.rels
.iter()
.any(|r| r.props.iter().any(|p| p.name().to_uppercase() == "ID"))
{
return Err(Error::ConfigItemReserved {
type_name: "ID".to_string(),
});
}
// Used by Gremlin return format to return the label itself
if t.rels
.iter()
.any(|r| r.props.iter().any(|p| p.name().to_uppercase() == "LABEL"))
{
return Err(Error::ConfigItemReserved {
type_name: "label".to_string(),
});
}
if t.rels
.iter()
.any(|r| r.props.iter().any(|p| p.name().to_uppercase() == "SRC"))
{
return Err(Error::ConfigItemReserved {
type_name: "src".to_string(),
});
}
if t.rels
.iter()
.any(|r| r.props.iter().any(|p| p.name().to_uppercase() == "DST"))
{
return Err(Error::ConfigItemReserved {
type_name: "dst".to_string(),
});
}
t.rels.iter().try_for_each(|r| {
let rel_name_variants = rel_name_variants(t, r);
self.model.iter().try_for_each(|t2| {
if rel_name_variants.contains(t2.name()) {
Err(Error::ConfigItemDuplicated {
type_name: t2.name().to_string(),
})
} else {
Ok(())
}
})
})?;
Ok(())
})
.collect::<Result<Vec<_>, Error>>()?;
self.endpoints
.iter()
.map(|ep| {
// Check for duplicate endpoints
if self.endpoints.iter().filter(|e| e.name == ep.name).count() > 1 {
return Err(Error::ConfigItemDuplicated {
type_name: ep.name.to_string(),
});
}
// Check for endpoint custom input using reserved names (GraphQL scalars)
if let Some(input) = &ep.input {
if let TypeDef::Custom(t) = &input.type_def {
if scalar_names.iter().any(|s| s == &t.name) {
return Err(Error::ConfigItemReserved {
type_name: t.name.to_string(),
});
}
}
}
// Check for endpoint custom input using reserved names (GraphQL scalars)
if let TypeDef::Custom(t) = &ep.output.type_def {
if scalar_names.iter().any(|s| s == &t.name) {
return Err(Error::ConfigItemReserved {
type_name: t.name.to_string(),
});
}
}
Ok(())
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(())
}
/// Returns the version number of the configuration format used for the configuration
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::Configuration;
///
/// let c = Configuration::new(1, Vec::new(), Vec::new());
///
/// assert_eq!(1, c.version());
/// ```
pub fn version(&self) -> i32 {
self.version
}
}
impl Default for Configuration {
fn default() -> Configuration {
Configuration {
version: 1,
model: vec![],
endpoints: vec![],
}
}
}
impl TryFrom<File> for Configuration {
type Error = Error;
fn try_from(f: File) -> Result<Configuration, Error> {
let r = BufReader::new(f);
Ok(serde_yaml::from_reader(r)?)
}
}
impl TryFrom<String> for Configuration {
type Error = Error;
fn try_from(s: String) -> Result<Configuration, Error> {
Ok(serde_yaml::from_str(&s)?)
}
}
impl TryFrom<&str> for Configuration {
type Error = Error;
fn try_from(s: &str) -> Result<Configuration, Error> {
Ok(serde_yaml::from_str(s)?)
}
}
/// Configuration item for custom endpoints
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{Endpoint, EndpointClass, EndpointType, TypeDef,
/// # GraphqlType};
///
/// let e = Endpoint::new("CountItems".to_string(), EndpointClass::Query, None,
/// EndpointType::new(TypeDef::Scalar(GraphqlType::Int), false, true));
/// ```
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Endpoint {
/// Name of this Endpoint
name: String,
/// Class of endpoint (Mutation or Query)
class: EndpointClass,
/// Defines the input of the endpoint
input: Option<EndpointType>,
/// Defines the type returned by the endpoint
output: EndpointType,
}
impl Endpoint {
/// Creates a new configuration item for a custom GraphQL endpoint
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{Endpoint, EndpointClass, EndpointType, TypeDef,
/// # GraphqlType};
///
/// let e = Endpoint::new("CountItems".to_string(), EndpointClass::Query, None,
/// EndpointType::new(TypeDef::Scalar(GraphqlType::Int), false, true));
/// ```
pub fn new(
name: String,
class: EndpointClass,
input: Option<EndpointType>,
output: EndpointType,
) -> Endpoint {
Endpoint {
name,
class,
input,
output,
}
}
/// Returns the [`EndpointClass`] of a custom GraphQL endpoint, indicating whether the custom
/// endpoint is a Query or Mutation.
///
/// [`EndpointClass`]: ./enum.EndpointClass.html
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{Endpoint, EndpointClass, EndpointType, TypeDef,
/// # GraphqlType};
///
/// let e = Endpoint::new("CountItems".to_string(), EndpointClass::Query, None,
/// EndpointType::new(TypeDef::Scalar(GraphqlType::Int), false, true));
///
/// assert_eq!(&EndpointClass::Query, e.class());
/// ```
pub fn class(&self) -> &EndpointClass {
&self.class
}
/// Returns the name of a custom GraphQL endpoint.
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{Endpoint, EndpointClass, EndpointType, TypeDef,
/// # GraphqlType};
///
/// let e = Endpoint::new("CountItems".to_string(), EndpointClass::Query, None,
/// EndpointType::new(TypeDef::Scalar(GraphqlType::Int), false, true));
///
/// assert_eq!("CountItems", e.name());
/// ```
pub fn name(&self) -> &str {
&self.name
}
/// Returns the optional type definition of the input to a custom endpoint. A value of None
/// indicates that the GraphQL endpoint does not take an input.
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{Endpoint, EndpointClass, EndpointType, TypeDef,
/// # GraphqlType};
///
/// let e = Endpoint::new("CountItems".to_string(), EndpointClass::Query, None,
/// EndpointType::new(TypeDef::Scalar(GraphqlType::Int), false, true));
///
/// assert!(e.input().is_none());
/// ```
pub fn input(&self) -> Option<&EndpointType> {
self.input.as_ref()
}
/// Returns the type definition of the output for a custom endpoint
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{Endpoint, EndpointClass, EndpointType, TypeDef,
/// # GraphqlType};
///
/// let e = Endpoint::new("CountItems".to_string(), EndpointClass::Query, None,
/// EndpointType::new(TypeDef::Scalar(GraphqlType::Int), false, true));
///
/// assert_eq!(&EndpointType::new(TypeDef::Scalar(GraphqlType::Int), false, true),
/// e.output());
/// ```
pub fn output(&self) -> &EndpointType {
&self.output
}
}
impl TryFrom<&str> for Endpoint {
type Error = Error;
/// Creates a new Endpoint struct from a yaml-formatted string.
///
/// # Errors
///
/// Returns an [`Error`] variant [`YamlDeserializationFailed`] if the yaml-formatted
/// string is improperly formatted.
///
/// [`YamlDeserializationFailed`]: ../../error/enum.Error.html#variant.YamlDeserializationFailed
/// [`Error`]: ../../error/enum.Error.html
///
/// # Examples
///
/// ```rust
/// use warpgrapher::engine::config::{Endpoint};
///
/// use std::convert::TryFrom;
/// let t = Endpoint::try_from("
/// name: RegisterUser
/// class: Mutation
/// input:
/// type: UserInput
/// output:
/// type: User
/// ").unwrap();
/// ```
fn try_from(yaml: &str) -> Result<Endpoint, Error> {
serde_yaml::from_str(yaml).map_err(|e| Error::YamlDeserializationFailed { source: e })
}
}
/// Determines whether a custom GraphQL endpoint is a query or mutation endpoint
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::EndpointClass;
///
/// let ec = EndpointClass::Query;
/// ```
#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum EndpointClass {
/// Indicates that a custome GraphQL endpoint should be a query
Query,
/// Indicates that a cutom GraphQL endpoint should be a mutation
Mutation,
}
/// Configuration item for endpoint filters. This allows configuration to control which of the
/// basic create, read, update, and delete (CRUD) operations are auto-generated for a [`Type`] or a
/// [`Relationship`]. If a filter boolean is set to true, the operation is generated. False
/// indicates that the operation should not be generated.
///
/// [`Relationship`]: ./struct.Relationship.html
/// [`Type`]: ./struct.Type.html
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::EndpointsFilter;
///
/// let ef = EndpointsFilter::new(true, true, true, true);
/// ```
#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct EndpointsFilter {
/// True if a read endpoint should be generated for the corresponding type/rel
#[serde(default = "get_true")]
read: bool,
/// True if a create endpoint should be generated for the corresponding type/rel
#[serde(default = "get_true")]
create: bool,
/// True if a update endpoint should be generated for the corresponding type/rel
#[serde(default = "get_true")]
update: bool,
/// True if a delete endpoint should be generated for the corresponding type/rel
#[serde(default = "get_true")]
delete: bool,
}
impl EndpointsFilter {
/// Creates a new filter with the option to configure all endpoints
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::EndpointsFilter;
///
/// let ef = EndpointsFilter::new(true, true, false, false);
/// ```
pub fn new(read: bool, create: bool, update: bool, delete: bool) -> EndpointsFilter {
EndpointsFilter {
read,
create,
update,
delete,
}
}
/// Creates a new filter with all endpoints -- create, read, update, and delete
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::EndpointsFilter;
///
/// let ef = EndpointsFilter::all();
/// ```
pub fn all() -> EndpointsFilter {
EndpointsFilter {
read: true,
create: true,
update: true,
delete: true,
}
}
/// Returns true if Warpgrapher should generate a create operation for the relationship
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::EndpointsFilter;
///
/// let ef = EndpointsFilter::all();
/// assert_eq!(true, ef.create());
/// ```
pub fn create(self) -> bool {
self.create
}
/// Returns true if Warpgrapher should generate a delete operation for the relationship
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::EndpointsFilter;
///
/// let ef = EndpointsFilter::all();
/// assert_eq!(true, ef.delete());
/// ```
pub fn delete(self) -> bool {
self.delete
}
/// Creates a new filter with all endpoints -- create, read, update, and delete -- filtered out
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::EndpointsFilter;
///
/// let ef = EndpointsFilter::none();
/// ```
pub fn none() -> EndpointsFilter {
EndpointsFilter {
read: false,
create: false,
update: false,
delete: false,
}
}
/// Returns true if Warpgrapher should generate a read operation for the relationship
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::EndpointsFilter;
///
/// let ef = EndpointsFilter::all();
/// assert_eq!(true, ef.read());
/// ```
pub fn read(self) -> bool {
self.read
}
/// Returns true if Warpgrapher should generate a update operation for the relationship
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::EndpointsFilter;
///
/// let ef = EndpointsFilter::all();
/// assert_eq!(true, ef.update());
/// ```
pub fn update(self) -> bool {
self.update
}
}
impl Default for EndpointsFilter {
fn default() -> EndpointsFilter {
EndpointsFilter {
read: true,
create: true,
update: true,
delete: true,
}
}
}
/// Configuration item describing a type used with a custom GraphQL endpoint, either as the input
/// to the custom endpoint, or as its output
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{EndpointType, GraphqlType, TypeDef};
///
/// let et = EndpointType::new(TypeDef::Scalar(GraphqlType::Int), false, true);
/// ```
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EndpointType {
/// Defines option for an endpoint type to use an existing or custom type
#[serde(rename = "type")]
type_def: TypeDef,
/// Determines if the endpoint type is a list
#[serde(default = "get_false")]
list: bool,
/// Determine if the type is required (non-nullable)
#[serde(default = "get_false")]
required: bool,
}
impl EndpointType {
/// Creates a new [`EndpointType`] configuration item, describing either the input or output
/// type of a custom GraphQL endpoint
///
/// [`EndpointType`]: ./struct.EndpointType.html
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{EndpointType, GraphqlType, TypeDef};
///
/// let et = EndpointType::new(TypeDef::Scalar(GraphqlType::Int), false, true);
/// ```
pub fn new(type_def: TypeDef, list: bool, required: bool) -> EndpointType {
EndpointType {
type_def,
list,
required,
}
}
/// True if the type associated with a custom GraphQL endpoint is a list of elements; false if
/// the type is a single value
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{EndpointType, GraphqlType, TypeDef};
///
/// let et = EndpointType::new(TypeDef::Scalar(GraphqlType::Int), false, true);
///
/// assert_eq!(false, et.list());
/// ```
pub fn list(&self) -> bool {
self.list
}
/// True if the type associated with a custom GraphQL endpoint is required; false if it is
/// optional
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{EndpointType, GraphqlType, TypeDef};
///
/// let et = EndpointType::new(TypeDef::Scalar(GraphqlType::Int), false, true);
///
/// assert_eq!(true, et.required());
/// ```
pub fn required(&self) -> bool {
self.required
}
/// Returns a [`TypeDef`] enumeration, describing whether the type is a GraphQL scalar type,
/// an existing type defined elsewhere in the configuration, or a new custom type.
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{EndpointType, GraphqlType, TypeDef};
///
/// let et = EndpointType::new(TypeDef::Scalar(GraphqlType::Int), false, true);
///
/// assert_eq!(&TypeDef::Scalar(GraphqlType::Int), et.type_def());
/// ```
pub fn type_def(&self) -> &TypeDef {
&self.type_def
}
}
/// Enumeration representing Graphql scalar types
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::GraphqlType;
///
/// let gt = GraphqlType::Int;
/// ```
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum GraphqlType {
/// GraphQL integer
Int,
/// GraphQL floating point number
Float,
/// GraphQL string value
String,
/// GraphQL boolean value
Boolean,
}
/// Configuration item for a property on a GraphQL type, modeled as properties on a graph node.
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{Property, UsesFilter};
///
/// let p = Property::new("name".to_string(), UsesFilter::all(), "String".to_string(), true,
/// false, None, None);
/// ```
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Property {
/// Name of the property
name: String,
/// Filter of properties that determines whether the property will be used in creation,
/// matching/query, update, and output/shape portions of the schema
#[serde(default)]
uses: UsesFilter,
/// The name of the type of the property (e.g. String)
#[serde(rename = "type")]
type_name: String,
/// True if this property is required to be present on this type; false if the property is
/// optional
#[serde(default = "get_true")]
required: bool,
/// True if this property is a list
#[serde(default = "get_false")]
list: bool,
/// The name of the resolver function to be called when querying for the value of this prop.
/// If this field is None, the prop resolves the scalar value from the database.
#[serde(default = "get_none")]
resolver: Option<String>,
/// The name of the validator function to be called when creating or modifying the value of
/// this prop. If this field is None, the prop resolves the scalar value from the database.
#[serde(default = "get_none")]
validator: Option<String>,
}
impl Property {
/// Creates a new Property struct.
///
/// # Arguments
///
/// * a String for the name of the property
/// * a String for the type of the property
/// * a boolean that, if true, indicatees that the property is mandatory, and if false, that
/// the property is optional
/// * a boolean that, if true, indicates that the property is a list of scalers, and if false,
/// that the property is a single value
/// * an optional string providing the name of a resolver, if the property is a dynamic
/// property with a custom resolver, and
/// * an optional string providing the name of a custom validator
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{Property, UsesFilter};
///
/// let p = Property::new("name".to_string(), UsesFilter::all(), "String".to_string(), true,
/// false, None, None);
/// ```
pub fn new(
name: String,
uses: UsesFilter,
type_name: String,
required: bool,
list: bool,
resolver: Option<String>,
validator: Option<String>,
) -> Property {
Property {
name,
uses,
type_name,
required,
list,
resolver,
validator,
}
}
/// Returns a boolean that if true, indicates that this property contains a list of scalar
/// values, and if false, indicates that the property contains only one value (or potentially
/// zero values if required is also false).
///
/// # Examples
///
/// # use warpgrapher::engine::config::Property;
///
/// let p = Property::new("name".to_string(), UsesFiter::all(), "String".to_string(),
/// true, false, None, None);
///
/// assert!(!p.list());
pub fn list(&self) -> bool {
self.list
}
/// Returns the name of the property
///
/// # Examples
///
/// # use warpgrapher::engine::config::Property;
///
/// let p = Property::new("propname".to_string(), UsesFilter::all(), "String".to_string(),
/// true, false, None, None);
///
/// assert_eq!("propname", p.name());
pub fn name(&self) -> &str {
&self.name
}
/// Returns the filter describing how a property is to be used
///
/// # Examples
///
/// # use warpgrapher::engine::config::Property;
///
/// let p = Property::new("propname".to_string(), UsesFilter::all(), "String".to_string(),
/// true, false, None, None);
///
/// assert_eq!(UsesFilter::all(), p.uses());
pub fn uses(&self) -> UsesFilter {
self.uses
}
/// Returns the optional name of the custom resolver associated with this property
///
/// # Examples
///
/// # use warpgrapher::engine::config::Property;
///
/// let p = Property::new("propname".to_string(), "String".to_string(), true, false,
/// Some("CustomResolver".to_string()), None);
///
/// assert_eq!("CustomResolver", p.resolver().unwrap());
pub fn resolver(&self) -> Option<&String> {
self.resolver.as_ref()
}
/// Returns a boolean that if true, indicates that this property is mandatory, and if false,
/// that the property is not required, and may be absent.
///
/// # Examples
///
/// # use warpgrapher::engine::config::Property;
///
/// let p = Property::new("name".to_string(), "String".to_string(), true, false, None, None);
///
/// assert!(p.required());
pub fn required(&self) -> bool {
self.required
}
/// Returns the name of the type of the property
///
/// # Examples
///
/// # use warpgrapher::engine::config::Property;
///
/// let p = Property::new("propname".to_string(), "String".to_string(), true, false, None,
/// None);
///
/// assert_eq!("String", p.type_name());
pub fn type_name(&self) -> &str {
&self.type_name
}
/// Returns the optional name of the custom validator associated with this property
///
/// # Examples
///
/// # use warpgrapher::engine::config::Property;
///
/// let p = Property::new("propname".to_string(), "String".to_string(), true, false,
/// None, Some("CustomValidator".to_string()));
///
/// assert_eq!("CustomValidator", p.validator().unwrap());
pub fn validator(&self) -> Option<&String> {
self.validator.as_ref()
}
}
/// Configuration item for a relationship on a GraphQL type
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{Relationship, EndpointsFilter};
///
/// let r = Relationship::new(
/// "teams".to_string(),
/// true,
/// vec!["User".to_string()],
/// vec![],
/// EndpointsFilter::all(),
/// None
/// );
/// ```
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Relationship {
/// Name of the relationship
name: String,
/// True if its a multi-node relationship
#[serde(default = "get_false")]
list: bool,
/// List of possible dst nodes for the relationship. A single element
/// vector indicates a single-type rel and more than one element
/// indicates a multi-type relationship.
nodes: Vec<String>,
/// Properties of the relationship
#[serde(default)]
props: Vec<Property>,
/// Filter of endpoints that determines which CRUD endpoints will be
/// auto generated for the relationship
#[serde(default)]
endpoints: EndpointsFilter,
/// The name of the resolver function to be called when querying for the value of this prop.
/// If this field is None, the prop resolves the scalar value from the database.
#[serde(default = "get_none")]
resolver: Option<String>,
}
impl Relationship {
/// Creates a new Relationship struct.
///
/// # Arguments
///
/// * a String for the name of the relationship
/// * a boolean that, if true, indicates that the property is a list, and if false, that the
/// relationship is to a single node
/// * a list of possible destination node types for the relationship. A single element in the
/// list indicates that this is a single-type relationship, whereas more than one element
/// indicates a multi-type relationship.
/// * an [`EndpointsFilter`] struct indicating which of the standard operations (create, read,
/// update, and delete) should be generated for this relationship
/// * an optional string providing the name of a resolver, if the relationship is a dynamic
/// relationship with a custom resolver
///
/// [`EndpointsFilter`]: ./struct.EndpointsFilter.html
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{EndpointsFilter, Relationship};
///
/// let r = Relationship::new("name".to_string(), false, vec!["User".to_string()], vec![],
/// EndpointsFilter::all(), None);
/// ```
pub fn new(
name: String,
list: bool,
nodes: Vec<String>,
props: Vec<Property>,
endpoints: EndpointsFilter,
resolver: Option<String>,
) -> Relationship {
Relationship {
name,
list,
nodes,
props,
endpoints,
resolver,
}
}
/// Returns the [`EndpointsFilter`] struct that indicates which of the four basic Create, Read,
/// Update, and Delete (CRUD) operations Warpgrapher should auto-generate for this
/// relationship.
///
/// [`EndpointsFilter`]: ./struct.EndpointsFilter.html
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{EndpointsFilter, Relationship};
///
/// let r = Relationship::new("name".to_string(), false, vec!["User".to_string()], vec![],
/// EndpointsFilter::all(), None);
///
/// assert_eq!(&EndpointsFilter::all(), r.endpoints());
/// ```
pub fn endpoints(&self) -> &EndpointsFilter {
&self.endpoints
}
/// Returns true if the relationship is a list, indicating a one-to-many (or many-to-many)
/// relationship. Returns false if the node can only have one relationship of this type.
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{EndpointsFilter, Relationship};
///
/// let r = Relationship::new("name".to_string(), true, vec!["User".to_string()], vec![],
/// EndpointsFilter::all(), None);
///
/// assert!(r.list());
/// ```
pub fn list(&self) -> bool {
self.list
}
/// Returns the name of the relationship
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{EndpointsFilter, Relationship};
///
/// let r = Relationship::new("RelName".to_string(), true, vec!["User".to_string()], vec![],
/// EndpointsFilter::all(), None);
///
/// assert_eq!("RelName", r.name());
/// ```
pub fn name(&self) -> &str {
&self.name
}
/// Returns an iterator over the names of the Warpgrapher [`Type`] definitions that are
/// possible destination nodes for this relationship.
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{EndpointsFilter, Relationship};
///
/// let r = Relationship::new("RelName".to_string(), true, vec!["User".to_string()], vec![],
/// EndpointsFilter::all(), None);
///
/// assert_eq!(1, r.nodes().count());
/// assert_eq!("User", r.nodes().next().expect("Expected an element"));
/// ```
pub fn nodes(&self) -> Iter<String> {
self.nodes.iter()
}
/// Returns a slice of [`Property`] references for properties associated with the relationship.
///
/// [`Property`]: ./struct.Property.html
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{EndpointsFilter, Relationship};
///
/// let r = Relationship::new("RelName".to_string(), true, vec!["User".to_string()], vec![],
/// EndpointsFilter::all(), None);
///
/// assert_eq!(0, r.props_as_slice().len())
/// ```
pub fn props_as_slice(&self) -> &[Property] {
&self.props
}
/// Returns an option for a string containing the name of a custom resolver, if this
/// relationship is resolved by a custom resolver instead of an auto-generated read resolver.
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{EndpointsFilter, Relationship};
///
/// let r = Relationship::new("RelName".to_string(), true, vec!["User".to_string()], vec![],
/// EndpointsFilter::all(), None);
///
/// assert!(r.resolver().is_none())
/// ```
pub fn resolver(&self) -> Option<&String> {
self.resolver.as_ref()
}
}
/// Configuration item for a GraphQL type. In back-end storage, the type is recorded in a label
/// attached to the graph node.
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{Type, Property, EndpointsFilter, UsesFilter};
///
/// let t = Type::new(
/// "User".to_string(),
/// vec!(Property::new("name".to_string(), UsesFilter::all(), "String".to_string(),
/// true, false, None, None),
/// Property::new("role".to_string(), UsesFilter::all(), "String".to_string(),
/// true, false, None, None)),
/// vec!(),
/// EndpointsFilter::all()
/// );
/// ```
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Type {
/// Name of this GraphQL type, also used as the label for nodes
name: String,
/// Vector of properties on this type
#[serde(default)]
props: Vec<Property>,
/// Vector of relationships on this type
#[serde(default)]
rels: Vec<Relationship>,
/// Filter of endpoints that determines which CRUD endpoints will be
/// auto generated for the relationship
#[serde(default)]
endpoints: EndpointsFilter,
}
impl Type {
/// Creates a new Type struct.
///
/// # Arguments
///
/// * name - the name of the type, which will be recorded as the label on a node in the graph
/// back-end
/// * props - a vector of [`Property`] structs describing the properties on the node
/// * rels - a vector of [`Relationship`] structs describing the outgoing relationships from
/// this node type
/// * endpoints - an [`EndpointsFilter`] struct describing which CRUD operations should be
/// generated automatically for this node type.
///
/// [`EndpointsFilter`]: ./struct.EndpointsFilter.html
/// [`Property`]: ./struct.Property.html
/// [`Type`]: ./struct.Type.html
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{Type, Property, EndpointsFilter, UsesFilter};
///
/// let t = Type::new(
/// "User".to_string(),
/// vec!(Property::new("name".to_string(), UsesFilter::all(), "String".to_string(),
/// true, false, None, None),
/// Property::new("role".to_string(), UsesFilter::all(), "String".to_string(),
/// true, false, None, None)),
/// vec!(),
/// EndpointsFilter::all()
/// );
/// ```
pub fn new(
name: String,
props: Vec<Property>,
rels: Vec<Relationship>,
endpoints: EndpointsFilter,
) -> Type {
Type {
name,
props,
rels,
endpoints,
}
}
/// Returns the name of the type. This type name is used as the label on nodes of this type in
/// the graph database storage back-end.
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{Type, Property, EndpointsFilter, UsesFilter};
///
/// let t = Type::new(
/// "User".to_string(),
/// vec!(Property::new("name".to_string(), UsesFilter::all(), "String".to_string(), true, false, None, None),
/// Property::new("role".to_string(), UsesFilter::all(), "String".to_string(), true, false, None, None)),
/// vec!(),
/// EndpointsFilter::all()
/// );
///
/// assert_eq!("User", t.name());
/// ```
pub fn name(&self) -> &str {
&self.name
}
/// Returns the [`EndpointsFilter`] struct associate with this type, determining which CRUD
/// operations should be auto-generated for this node type.
///
/// [`EndpointsFilter`]: ./struct.EndpointsFilter.html
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{Type, Property, EndpointsFilter, UsesFilter};
///
/// let t = Type::new(
/// "User".to_string(),
/// vec!(Property::new("name".to_string(), UsesFilter::all(), "String".to_string(), true, false, None, None),
/// Property::new("role".to_string(), UsesFilter::all(), "String".to_string(), true, false, None, None)),
/// vec!(),
/// EndpointsFilter::all()
/// );
///
/// assert_eq!(&EndpointsFilter::all(), t.endpoints());
/// ```
pub fn endpoints(&self) -> &EndpointsFilter {
&self.endpoints
}
/// Returns an iterator over the [`Property`] structs defining properties on this node type.
///
/// [`Property`]: ./struct.Property.html
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{Type, Property, EndpointsFilter, UsesFilter};
///
/// let t = Type::new(
/// "User".to_string(),
/// vec!(Property::new("name".to_string(), UsesFilter::all(), "String".to_string(),
/// true, false, None, None)), vec!(), EndpointsFilter::all());
///
/// assert_eq!("name", t.props().next().expect("Expected property").name());
/// ```
pub fn props(&self) -> Iter<Property> {
self.props.iter()
}
pub fn mut_props(&mut self) -> &mut Vec<Property> {
&mut self.props
}
/// Returns a slice of the [`Property`] structs defining properties on this node type.
///
/// [`Property`]: ./struct.Property.html
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{Type, Property, EndpointsFilter, UsesFilter};
///
/// let t = Type::new(
/// "User".to_string(),
/// vec!(Property::new("name".to_string(), UsesFilter::all(), "String".to_string(), true, false, None, None)),
/// vec!(),
/// EndpointsFilter::all()
/// );
///
/// assert_eq!("name", t.props_as_slice()[0].name());
/// ```
pub fn props_as_slice(&self) -> &[Property] {
&self.props
}
/// Returns an iterator over the [`Relationship`] structs defining relationships originating
/// from this node type.
///
/// [`Relationship`]: ./struct.Relationship.html
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{EndpointsFilter, Property, Relationship,
/// # Type, UsesFilter};
///
/// let t = Type::new(
/// "User".to_string(),
/// vec!(Property::new("name".to_string(), UsesFilter::all(), "String".to_string(), true,
/// false, None, None)),
/// vec!(Relationship::new("rel_name".to_string(), false, vec!("Role".to_string()), vec!(
/// Property::new("rel_prop".to_string(), UsesFilter::all(), "String".to_string(), true, false, None, None)
/// ), EndpointsFilter::all(), None)),
/// EndpointsFilter::all()
/// );
///
/// assert_eq!("rel_name", t.rels().next().expect("Expected relationship").name());
/// ```
pub fn rels(&self) -> Iter<Relationship> {
self.rels.iter()
}
}
impl TryFrom<&str> for Type {
type Error = Error;
/// Creates a new Type struct from a yaml-formatted string.
///
/// # Errors
///
/// Returns an [`Error`] variant [`YamlDeserializationFailed`] if the yaml-formatted
/// string is improperly formatted.
///
/// [`YamlDeserializationFailed`]: ../../error/enum.Error.html#variant.YamlDeserializationFailed
/// [`Error`]: ../../error/enum.Error.html
///
/// # Examples
///
/// ```rust
/// use warpgrapher::engine::config::{Type};
///
/// use std::convert::TryFrom;
/// let t = Type::try_from("
/// name: User
/// props:
/// - name: name
/// type: String
/// ").unwrap();
/// ```
fn try_from(yaml: &str) -> Result<Type, Error> {
serde_yaml::from_str(yaml).map_err(|e| Error::YamlDeserializationFailed { source: e })
}
}
/// Enumeration representing the definition of a type used as the optional input or the output for
/// a custom GraphQL endpooint
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{GraphqlType, TypeDef};
///
/// let td = TypeDef::Scalar(GraphqlType::Int);
/// ```
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(untagged)]
pub enum TypeDef {
/// The type is a GraphQL scalar type. The tuple value indicates the scalar type to use.
Scalar(GraphqlType),
/// The type is an existing type, already defined by the configuration file or auto-generated
/// by Warpgrapher. The tuple value is the name of the existing type to use.
Existing(String),
/// The type is a new custom type, defined for this custom endpoint. The tuple value is a
/// [`Type`] struct defining the custom type.alloc
///
/// [`Type`]: ./struct.Type.html
Custom(Type),
}
/// Configuration item for property usage filters. This allows configuration to control which of the
/// basic creation input, query input, update input, and output uses are auto-generated for a
/// [`Property`]. If a filter boolean is set to true, the use of the property is generated. False
/// indicates that the property should be omitted from use. For example, one might set the output
/// use to true and all other uses to false for calculated value that is derived upon request but
/// would never appear in the creation or update of a node. If all are set to false, the property
/// is hidden, meaning that it can be read from and written to the database but does not appear in
/// the client-facing GraphQL schema.
///
/// [`Property`]: ./struct.Property.html
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::UsesFilter;
///
/// let uf = UsesFilter::new(true, true, true, true);
/// ```
#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct UsesFilter {
/// True if the property should be included in the NodeCreateMutationInput portion of the schema
#[serde(default = "get_true")]
create: bool,
/// True if the property should be included in the NodeQueryInput portion of the schema
#[serde(default = "get_true")]
query: bool,
/// True if the property should be included in the NodeUpdateMutationInput portion of the schema
#[serde(default = "get_true")]
update: bool,
/// True if the property should be included in output shape portion of the schema
#[serde(default = "get_true")]
output: bool,
}
impl UsesFilter {
/// Creates a new filter with the option to configure uses of a property
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::UsesFilter;
///
/// let uf = UsesFilter::new(false, false, false, true);
/// ```
pub fn new(create: bool, query: bool, update: bool, output: bool) -> UsesFilter {
UsesFilter {
create,
query,
update,
output,
}
}
/// Creates a new filter with all uses -- create, query, update, and output
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::UsesFilter;
///
/// let uf = UsesFilter::all();
/// ```
pub fn all() -> UsesFilter {
UsesFilter {
create: true,
query: true,
update: true,
output: true,
}
}
/// Returns true if Warpgrapher should use the property in the NodeCreateMutationInput
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::UsesFilter;
///
/// let uf = UsesFilter::all();
/// assert_eq!(true, uf.create());
/// ```
pub fn create(self) -> bool {
self.create
}
/// Returns true if Warpgrapher should use the property in the NodeQueryInput
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::UsesFilter;
///
/// let uf = UsesFilter::all();
/// assert_eq!(true, uf.query());
/// ```
pub fn query(self) -> bool {
self.query
}
/// Creates a new filter with no uses of a property, hiding it from the GraphQL schema
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::UsesFilter;
///
/// let uf = UsesFilter::none();
/// ```
pub fn none() -> UsesFilter {
UsesFilter {
create: false,
query: false,
update: false,
output: false,
}
}
/// Returns true if Warpgrapher should generate a property in the output shape of a node
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::UsesFilter;
///
/// let uf = UsesFilter::all();
/// assert_eq!(true, uf.output());
/// ```
pub fn output(self) -> bool {
self.output
}
/// Returns true if Warpgrapher should use the property in the NodeUpdateInput
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::UsesFilter;
///
/// let uf = UsesFilter::all();
/// assert_eq!(true, uf.update());
/// ```
pub fn update(self) -> bool {
self.update
}
}
impl Default for UsesFilter {
fn default() -> UsesFilter {
UsesFilter {
create: true,
query: true,
update: true,
output: true,
}
}
}
/// Creates a combined [`Configuration`] data structure from multiple [`Configuration`] structs.
/// All [`Configuration`] structs must be the same version.
///
/// Returns a `Result<Configuration, Error>` with a single [`Configuration`] struct, or if the
/// versions across all `Configuration`s do not match, a [`ConfigVersionMismatched`] error.
///
/// [`Configuration`]: struct.Configuration.html
/// [`ConfigVersionMismatched`]: ../enum.Error.html#variant.ConfigVersionMismatched
///
///
/// # Examples
///
/// ```rust
/// # use warpgrapher::engine::config::{Configuration, compose};
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut config_vec: Vec<Configuration> = Vec::new();
/// let config = compose(config_vec)?;
/// # Ok(())
/// # }
/// ```
pub fn compose(configs: Vec<Configuration>) -> Result<Configuration, Error> {
let mut version: Option<i32> = None;
let mut model: Vec<Type> = Vec::new();
let mut endpoints: Vec<Endpoint> = Vec::new();
configs
.into_iter()
.map(|mut c| {
match version {
None => version = Some(c.version()),
Some(v) => {
if v != c.version {
return Err(Error::ConfigVersionMismatched {
expected: v,
found: c.version,
});
}
}
}
model.append(&mut c.model);
endpoints.append(&mut c.endpoints);
Ok(())
})
.collect::<Result<Vec<_>, Error>>()?;
// There will be no version number if the vector of Configurations is empty, in which case
// we might as well use the latest version
Ok(Configuration::new(
version.unwrap_or(LATEST_CONFIG_VERSION),
model,
endpoints,
))
}
#[cfg(test)]
pub(crate) fn mock_project_config() -> Configuration {
Configuration::new(1, vec![mock_project_type()], vec![])
}
#[cfg(test)]
pub(crate) fn mock_project_type() -> Type {
Type::new(
"Project".to_string(),
vec![
Property::new(
"name".to_string(),
UsesFilter::all(),
"String".to_string(),
true,
false,
None,
None,
),
Property::new(
"tags".to_string(),
UsesFilter::all(),
"String".to_string(),
false,
true,
None,
None,
),
Property::new(
"public".to_string(),
UsesFilter::all(),
"Boolean".to_string(),
true,
false,
None,
None,
),
],
vec![
Relationship::new(
"owner".to_string(),
false,
vec!["User".to_string()],
vec![Property::new(
"since".to_string(),
UsesFilter::all(),
"String".to_string(),
false,
false,
None,
None,
)],
EndpointsFilter::all(),
None,
),
Relationship::new(
"board".to_string(),
false,
vec!["ScrumBoard".to_string(), "KanbanBoard".to_string()],
vec![],
EndpointsFilter::all(),
None,
),
Relationship::new(
"commits".to_string(),
true,
vec!["Commit".to_string()],
vec![],
EndpointsFilter::all(),
None,
),
Relationship::new(
"issues".to_string(),
true,
vec!["Feature".to_string(), "Bug".to_string()],
vec![],
EndpointsFilter::all(),
None,
),
],
EndpointsFilter::all(),
)
}
#[cfg(test)]
fn mock_user_type() -> Type {
Type::new(
"User".to_string(),
vec![Property::new(
"name".to_string(),
UsesFilter::all(),
"String".to_string(),
true,
false,
None,
None,
)],
vec![],
EndpointsFilter::all(),
)
}
#[cfg(test)]
fn mock_kanbanboard_type() -> Type {
Type::new(
"KanbanBoard".to_string(),
vec![Property::new(
"name".to_string(),
UsesFilter::all(),
"String".to_string(),
true,
false,
None,
None,
)],
vec![],
EndpointsFilter::all(),
)
}
#[cfg(test)]
fn mock_scrumboard_type() -> Type {
Type::new(
"ScrumBoard".to_string(),
vec![Property::new(
"name".to_string(),
UsesFilter::all(),
"String".to_string(),
true,
false,
None,
None,
)],
vec![],
EndpointsFilter::all(),
)
}
#[cfg(test)]
fn mock_feature_type() -> Type {
Type::new(
"Feature".to_string(),
vec![Property::new(
"name".to_string(),
UsesFilter::all(),
"String".to_string(),
true,
false,
None,
None,
)],
vec![],
EndpointsFilter::all(),
)
}
#[cfg(test)]
fn mock_bug_type() -> Type {
Type::new(
"Bug".to_string(),
vec![Property::new(
"name".to_string(),
UsesFilter::all(),
"String".to_string(),
true,
false,
None,
None,
)],
vec![],
EndpointsFilter::all(),
)
}
#[cfg(test)]
fn mock_commit_type() -> Type {
Type::new(
"Commit".to_string(),
vec![Property::new(
"name".to_string(),
UsesFilter::all(),
"String".to_string(),
true,
false,
None,
None,
)],
vec![],
EndpointsFilter::all(),
)
}
#[cfg(test)]
pub(crate) fn mock_endpoint_one() -> Endpoint {
// RegisterUsers(input: [UserCreateMutationInput]): [User]
Endpoint::new(
"RegisterUsers".to_string(),
EndpointClass::Mutation,
Some(EndpointType::new(
TypeDef::Existing("UserCreateMutationInput".to_string()),
true,
true,
)),
EndpointType::new(TypeDef::Existing("User".to_string()), true, true),
)
}
#[cfg(test)]
pub(crate) fn mock_endpoint_two() -> Endpoint {
// DisableUser(input: UserQueryInput): User
Endpoint::new(
"DisableUser".to_string(),
EndpointClass::Mutation,
Some(EndpointType::new(
TypeDef::Existing("UserQueryInput".to_string()),
false,
true,
)),
EndpointType::new(TypeDef::Existing("User".to_string()), false, true),
)
}
#[cfg(test)]
pub(crate) fn mock_endpoint_three() -> Endpoint {
// ComputeBurndown(input: BurndownFilter): BurndownMetrics
Endpoint::new(
"ComputeBurndown".to_string(),
EndpointClass::Query,
Some(EndpointType::new(
TypeDef::Custom(Type::new(
"BurndownFilter".to_string(),
vec![Property::new(
"ticket_types".to_string(),
UsesFilter::all(),
"String".to_string(),
true,
false,
None,
None,
)],
vec![],
EndpointsFilter::all(),
)),
false,
false,
)),
EndpointType::new(
TypeDef::Custom(Type::new(
"BurndownMetrics".to_string(),
vec![Property::new(
"points".to_string(),
UsesFilter::all(),
"Int".to_string(),
false,
false,
None,
None,
)],
vec![],
EndpointsFilter::all(),
)),
false,
true,
),
)
}
#[cfg(test)]
pub(crate) fn mock_config() -> Configuration {
Configuration::new(
1,
vec![
mock_project_type(),
mock_user_type(),
mock_kanbanboard_type(),
mock_scrumboard_type(),
mock_feature_type(),
mock_bug_type(),
mock_commit_type(),
],
vec![
mock_endpoint_one(),
mock_endpoint_two(),
mock_endpoint_three(),
],
)
}
#[cfg(test)]
pub(crate) fn mock_endpoints_filter() -> Configuration {
Configuration::new(
1,
vec![Type::new(
"User".to_string(),
vec![Property::new(
"name".to_string(),
UsesFilter::all(),
"String".to_string(),
true,
false,
None,
None,
)],
vec![],
EndpointsFilter::new(false, true, false, false),
)],
Vec::new(),
)
}
#[cfg(test)]
mod tests {
use super::{
compose, Configuration, Endpoint, EndpointType, EndpointsFilter, Property, Relationship,
Type, UsesFilter,
};
use crate::Error;
use std::convert::TryInto;
use std::fs::File;
/// There's not really much of a "test" per se, in this first unit test.
/// This is the example used in the book/src/warpgrapher/config.md file, so
/// having it here is a forcing function to make sure we catch any changes
/// in interface that would change this code and update the book to match.
#[test]
fn warpgrapher_book_config() {
let config = Configuration::new(
1,
vec![
// User
Type::new(
"User".to_string(),
vec![
Property::new(
"username".to_string(),
UsesFilter::all(),
"String".to_string(),
false,
false,
None,
None,
),
Property::new(
"email".to_string(),
UsesFilter::all(),
"String".to_string(),
false,
false,
None,
None,
),
],
Vec::new(),
EndpointsFilter::all(),
),
// Team
Type::new(
"Team".to_string(),
vec![Property::new(
"teamname".to_string(),
UsesFilter::all(),
"String".to_string(),
false,
false,
None,
None,
)],
vec![Relationship::new(
"members".to_string(),
true,
vec!["User".to_string()],
Vec::new(),
EndpointsFilter::default(),
None,
)],
EndpointsFilter::all(),
),
],
vec![],
);
assert!(!config.model.is_empty());
}
/// Passes if a new Configuration is created
#[test]
fn new_warpgrapher_config() {
let c = Configuration::new(1, Vec::new(), Vec::new());
assert!(c.version == 1);
assert!(c.model.is_empty());
}
// Passes if a Property is created and prints correctly
#[test]
fn new_property() {
let p = Property::new(
"name".to_string(),
UsesFilter::all(),
"String".to_string(),
true,
false,
None,
None,
);
assert!(p.name == "name");
assert!(p.type_name == "String");
}
/// Passes if a Type is created
#[test]
fn new_node_type() {
let t = Type::new(
"User".to_string(),
vec![
Property::new(
"name".to_string(),
UsesFilter::all(),
"String".to_string(),
true,
false,
None,
None,
),
Property::new(
"role".to_string(),
UsesFilter::all(),
"String".to_string(),
true,
false,
None,
None,
),
],
vec![],
EndpointsFilter::all(),
);
assert!(t.name == "User");
assert!(t.props.get(0).unwrap().name == "name");
assert!(t.props.get(1).unwrap().name == "role");
}
#[allow(clippy::match_wild_err_arm)]
#[test]
fn test_validate() {
//Test valid config
let valid_config: Configuration =
match File::open("tests/fixtures/config-validation/test_config_ok.yml")
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
assert!(valid_config.validate().is_ok());
//Test composed config
let mut config_vec: Vec<Configuration> = Vec::new();
let valid_config_0: Configuration =
match File::open("tests/fixtures/config-validation/test_config_compose_0.yml")
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
let valid_config_1: Configuration =
match File::open("tests/fixtures/config-validation/test_config_compose_1.yml")
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
let valid_config_2: Configuration =
match File::open("tests/fixtures/config-validation/test_config_compose_2.yml")
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
config_vec.push(valid_config_0);
config_vec.push(valid_config_1);
config_vec.push(valid_config_2);
let composed_config: Configuration = match compose(config_vec) {
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
assert!(composed_config.validate().is_ok());
//Test duplicate Type
let duplicate_type_config: Configuration =
match File::open("tests/fixtures/config-validation/test_config_duplicate_type.yml")
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match duplicate_type_config.validate() {
Err(Error::ConfigItemDuplicated { type_name: _ }) => (),
_ => panic!(),
}
//Test duplicate Endpoint type
let duplicate_endpoint_config: Configuration =
match File::open("tests/fixtures/config-validation/test_config_duplicate_endpoint.yml")
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match duplicate_endpoint_config.validate() {
Err(Error::ConfigItemDuplicated { type_name: _ }) => (),
_ => panic!(),
}
let duplicate_derived_name_config: Configuration = match File::open(
"tests/fixtures/config-validation/test_config_duplicate_derived_name.yml",
)
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match duplicate_derived_name_config.validate() {
Err(Error::ConfigItemDuplicated { type_name: _ }) => (),
_ => panic!(),
}
}
#[allow(clippy::match_wild_err_arm)]
#[test]
fn config_prop_name_id_test() {
let node_prop_name_id_config: Configuration =
match File::open("tests/fixtures/config-validation/test_config_node_prop_name_id.yml")
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match node_prop_name_id_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
let rel_prop_name_id_config: Configuration =
match File::open("tests/fixtures/config-validation/test_config_rel_prop_name_id.yml")
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match rel_prop_name_id_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
}
#[allow(clippy::match_wild_err_arm)]
#[test]
fn config_prop_name_src_test() {
let rel_prop_name_src_config: Configuration =
match File::open("tests/fixtures/config-validation/test_config_rel_prop_name_src.yml")
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match rel_prop_name_src_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
}
#[allow(clippy::match_wild_err_arm)]
#[test]
fn config_prop_name_dst_test() {
let rel_prop_name_dst_config: Configuration =
match File::open("tests/fixtures/config-validation/test_config_rel_prop_name_dst.yml")
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match rel_prop_name_dst_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
}
#[allow(clippy::match_wild_err_arm)]
#[test]
fn config_scalar_name_int_test() {
//Test Scalar Type Name: Int
let scalar_type_name_int_config: Configuration = match File::open(
"tests/fixtures/config-validation/test_config_scalar_type_name_int.yml",
)
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match scalar_type_name_int_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
//Test Scalar Endpoint Input Name: Int
let scalar_endpoint_input_type_name_int_config: Configuration = match File::open(
"tests/fixtures/config-validation/test_config_scalar_endpoint_input_type_name_int.yml",
)
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match scalar_endpoint_input_type_name_int_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
//Test Scalar Endpoint Output Name: Int
let scalar_endpoint_output_type_name_int_config: Configuration = match File::open(
"tests/fixtures/config-validation/test_config_scalar_endpoint_output_type_name_int.yml",
)
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match scalar_endpoint_output_type_name_int_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
}
#[allow(clippy::match_wild_err_arm)]
#[test]
fn config_scalar_name_float_test() {
//Test Scalar Type Name: Float
let scalar_type_name_float_config: Configuration = match File::open(
"tests/fixtures/config-validation/test_config_scalar_type_name_float.yml",
)
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match scalar_type_name_float_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
//Test Scalar Endpoint Input Name: Float
let scalar_endpoint_input_type_name_float_config: Configuration = match File::open(
"tests/fixtures/config-validation/test_config_scalar_endpoint_input_type_name_float.yml",
)
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match scalar_endpoint_input_type_name_float_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
//Test Scalar Endpoint Output Name: Float
let scalar_endpoint_output_type_name_float_config: Configuration = match File::open(
"tests/fixtures/config-validation/test_config_scalar_endpoint_output_type_name_float.yml",
)
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match scalar_endpoint_output_type_name_float_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
}
#[allow(clippy::match_wild_err_arm)]
#[test]
fn config_scalar_name_string_test() {
//Test Scalar Type Name: String
let scalar_type_name_string_config: Configuration = match File::open(
"tests/fixtures/config-validation/test_config_scalar_type_name_string.yml",
)
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match scalar_type_name_string_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
//Test Scalar Endpoint Input Name: String
let scalar_endpoint_input_type_name_string_config: Configuration = match File::open(
"tests/fixtures/config-validation/test_config_scalar_endpoint_input_type_name_string.yml",
)
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match scalar_endpoint_input_type_name_string_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
//Test Scalar Endpoint Output Name: String
let scalar_endpoint_output_type_name_string_config: Configuration = match File::open(
"tests/fixtures/config-validation/test_config_scalar_endpoint_output_type_name_string.yml",
)
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match scalar_endpoint_output_type_name_string_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
}
#[allow(clippy::match_wild_err_arm)]
#[test]
fn config_scalar_name_id_test() {
//Test Scalar Type Name: ID
let scalar_type_name_id_config: Configuration = match File::open(
"tests/fixtures/config-validation/test_config_scalar_type_name_id.yml",
)
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match scalar_type_name_id_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
//Test Scalar Endpoint Input Name: id
let scalar_endpoint_input_type_name_id_config: Configuration = match File::open(
"tests/fixtures/config-validation/test_config_scalar_endpoint_input_type_name_id.yml",
)
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match scalar_endpoint_input_type_name_id_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
//Test Scalar Endpoint Output Name: ID
let scalar_endpoint_output_type_name_id_config: Configuration = match File::open(
"tests/fixtures/config-validation/test_config_scalar_endpoint_output_type_name_id.yml",
)
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match scalar_endpoint_output_type_name_id_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
}
#[allow(clippy::match_wild_err_arm)]
#[test]
fn config_scalar_name_boolean_test() {
//Test Scalar Type Name: Boolean
let scalar_type_name_boolean_config: Configuration = match File::open(
"tests/fixtures/config-validation/test_config_scalar_type_name_boolean.yml",
)
.expect("Coudln't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match scalar_type_name_boolean_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
//Test Scalar Endpoint Input Name: Boolean
let scalar_endpoint_input_type_name_boolean_config: Configuration = match File::open(
"tests/fixtures/config-validation/test_config_scalar_endpoint_input_type_name_boolean.yml",
)
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match scalar_endpoint_input_type_name_boolean_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
//Test Scalar Endpoint Output Name: Boolean
let scalar_endpoint_output_type_name_boolean_config: Configuration = match File::open(
"tests/fixtures/config-validation/test_config_scalar_endpoint_output_type_name_boolean.yml",
)
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
match scalar_endpoint_output_type_name_boolean_config.validate() {
Err(Error::ConfigItemReserved { type_name: _ }) => (),
_ => panic!(),
}
}
#[allow(clippy::match_wild_err_arm)]
#[test]
fn test_compose() {
assert!(TryInto::<Configuration>::try_into(
File::open("tests/fixtures/config-validation/test_config_err.yml")
.expect("Couldn't open file")
)
.is_err());
assert!(TryInto::<Configuration>::try_into(
File::open("tests/fixtures/config-validation/test_config_ok.yml")
.expect("Couldn't open file")
)
.is_ok());
let mut config_vec: Vec<Configuration> = Vec::new();
assert!(compose(config_vec.clone()).is_ok());
let valid_config_0: Configuration =
match File::open("tests/fixtures/config-validation/test_config_compose_0.yml")
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
let valid_config_1: Configuration =
match File::open("tests/fixtures/config-validation/test_config_compose_1.yml")
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
let valid_config_2: Configuration =
match File::open("tests/fixtures/config-validation/test_config_compose_2.yml")
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
let mismatch_version_config: Configuration =
match File::open("tests/fixtures/config-validation/test_config_with_version_100.yml")
.expect("Couldn't open file")
.try_into()
{
Err(e) => panic!("{}", e),
Ok(wgc) => wgc,
};
config_vec.push(valid_config_0);
config_vec.push(valid_config_1);
config_vec.push(valid_config_2);
assert!(compose(config_vec.clone()).is_ok());
config_vec.push(mismatch_version_config);
assert!(compose(config_vec).is_err());
}
/// Passes if Configuration implements the Send trait
#[test]
fn test_config_send() {
fn assert_send<T: Send>() {}
assert_send::<Configuration>();
}
/// Passes if Configuration implements the Sync trait
#[test]
fn test_config_sync() {
fn assert_sync<T: Sync>() {}
assert_sync::<Configuration>();
}
/// Passes if Endpoint implements the Send trait
#[test]
fn test_endpoint_send() {
fn assert_send<T: Send>() {}
assert_send::<Endpoint>();
}
/// Passes if Endpoint implements the Sync trait
#[test]
fn test_endpoint_sync() {
fn assert_sync<T: Sync>() {}
assert_sync::<Endpoint>();
}
/// Passes if EndpointsFilter implements the Send trait
#[test]
fn test_endpoints_filter_send() {
fn assert_send<T: Send>() {}
assert_send::<EndpointsFilter>();
}
/// Passes if EndpointsFilter implements the Sync trait
#[test]
fn test_endpoints_filter_sync() {
fn assert_sync<T: Sync>() {}
assert_sync::<EndpointsFilter>();
}
/// Passes if EndpointType implements the Send trait
#[test]
fn test_endpoints_type_send() {
fn assert_send<T: Send>() {}
assert_send::<EndpointType>();
}
/// Passes if EndpointType implements the Sync trait
#[test]
fn test_endpoints_type_sync() {
fn assert_sync<T: Sync>() {}
assert_sync::<EndpointType>();
}
/// Passes if Property implements the Send trait
#[test]
fn test_property_send() {
fn assert_send<T: Send>() {}
assert_send::<Property>();
}
/// Passes if Property implements the Sync trait
#[test]
fn test_property_sync() {
fn assert_sync<T: Sync>() {}
assert_sync::<Property>();
}
/// Passes if Relationship implements the Send trait
#[test]
fn test_relationship_send() {
fn assert_send<T: Send>() {}
assert_send::<Relationship>();
}
/// Passes if Relationship implements the Sync trait
#[test]
fn test_relationship_sync() {
fn assert_sync<T: Sync>() {}
assert_sync::<Relationship>();
}
/// Passes if Type implements the Send trait
#[test]
fn test_type_send() {
fn assert_send<T: Send>() {}
assert_send::<Type>();
}
/// Passes if Type implements the Sync trait
#[test]
fn test_type_sync() {
fn assert_sync<T: Sync>() {}
assert_sync::<Type>();
}
}