Input Validation

In many cases, it's necessary to ensure that inputs are valid. What constitutes a valid input is up to the application, but it may mean that values have to be less than a certain length, within a certain range, and/or include or exclude certain characters. Warpgrapher makes it possible to write custom validation functions to reject invalid inputs.

Configuration

In the configuration snippet below, the name property has a validator field with the name NameValidator. The NameValidator string will be used later to connect the Rust function with this definition in the schema.

version: 1
model:
  User
  - name: User
    props:
      - name: name
        type: String
        required: true
        validator: NameValidator

Implementation

The implementation below defines the input validation function itself. The function is relatively simple, rejecting the input if the name is "KENOBI". All other names are accepted.

fn name_validator(value: &Value) -> Result<(), Error> {
    if let Value::Map(m) = value {
        if let Some(Value::String(name)) = m.get("name") {
            if name == "KENOBI" {
                Err(Error::ValidationFailed {
                    message: format!(
                        "Input validator for {field_name} failed. Cannot be named KENOBI",
                        field_name = "name"
                    ),
                })
            } else {
                Ok(())
            }
        } else {
            Err(Error::ValidationFailed {
                message: format!(
                    "Input validator for {field_name} failed.",
                    field_name = "name"
                ),
            })
        }
    } else {
        Err(Error::ValidationFailed {
            message: format!(
                "Input validator for {field_name} failed.",
                field_name = "name"
            ),
        })
    }
}

Add Validators to the Engine

The validators, such as the one defined above, are packaged into a map from the name(s) used in the configuration to the Rust functions. The map is then provided to the Warpgrapher Engine as the engine is built.

    // load validators
    let mut validators: Validators = Validators::new();
    validators.insert("NameValidator".to_string(), Box::new(name_validator));

    // create warpgrapher engine
    let engine: Engine<AppRequestContext> = Engine::new(config, db)
        .with_validators(validators.clone())
        .build()
        .expect("Failed to build engine");

Example API Call

The follow example API call invokes the validator defined above.

    let query = "
        mutation {
            UserCreate(input: {
                name: \"KENOBI\"
            }) {
                id
                name
            }
        }
    "
    .to_string();
    let metadata = HashMap::new();
    let result = engine.execute(query, None, metadata).await.unwrap();

Full Example Source

See below for the full source code to the example above.

use std::collections::HashMap;
use std::convert::TryFrom;
use warpgrapher::engine::config::Configuration;
use warpgrapher::engine::context::RequestContext;
use warpgrapher::engine::database::cypher::CypherEndpoint;
use warpgrapher::engine::database::DatabaseEndpoint;
use warpgrapher::engine::validators::Validators;
use warpgrapher::engine::value::Value;
use warpgrapher::{Engine, Error};

static CONFIG: &str = "
version: 1
model:
  User
  - name: User
    props:
      - name: name
        type: String
        required: true
        validator: NameValidator
";

#[derive(Clone, Debug)]
struct AppRequestContext {}

impl RequestContext for AppRequestContext {
    type DBEndpointType = CypherEndpoint;
    fn new() -> AppRequestContext {
        AppRequestContext {}
    }
}

fn name_validator(value: &Value) -> Result<(), Error> {
    if let Value::Map(m) = value {
        if let Some(Value::String(name)) = m.get("name") {
            if name == "KENOBI" {
                Err(Error::ValidationFailed {
                    message: format!(
                        "Input validator for {field_name} failed. Cannot be named KENOBI",
                        field_name = "name"
                    ),
                })
            } else {
                Ok(())
            }
        } else {
            Err(Error::ValidationFailed {
                message: format!(
                    "Input validator for {field_name} failed.",
                    field_name = "name"
                ),
            })
        }
    } else {
        Err(Error::ValidationFailed {
            message: format!(
                "Input validator for {field_name} failed.",
                field_name = "name"
            ),
        })
    }
}

#[tokio::main]
async fn main() {
    // parse warpgrapher config
    let config = Configuration::try_from(CONFIG.to_string()).expect("Failed to parse CONFIG");

    // define database endpoint
    let db = CypherEndpoint::from_env()
        .expect("Failed to parse cypher endpoint from environment")
        .pool()
        .await
        .expect("Failed to create cypher database pool");

    // load validators
    let mut validators: Validators = Validators::new();
    validators.insert("NameValidator".to_string(), Box::new(name_validator));

    // create warpgrapher engine
    let engine: Engine<AppRequestContext> = Engine::new(config, db)
        .with_validators(validators.clone())
        .build()
        .expect("Failed to build engine");

    let query = "
        mutation {
            UserCreate(input: {
                name: \"KENOBI\"
            }) {
                id
                name
            }
        }
    "
    .to_string();
    let metadata = HashMap::new();
    let result = engine.execute(query, None, metadata).await.unwrap();

    println!("result: {:#?}", result);
}