odata_client_codegen 0.1.0

Strongly-typed OData client code generation
Documentation
use std::{
    fs,
    path::PathBuf,
    sync::mpsc::{self, RecvTimeoutError},
    thread,
    time::Duration,
};

use bumpalo::Bump;

use super::{
    construct::{construct_entity_model, ConstructConfig},
    identifiers::QualifiedName,
    EntityModel, EnumType, EnumTypeMember, EnumUnderlyingType,
};

const SERVICE_URL: &str = "http://services.odata.org/V4/TripPinService";

#[test]
fn trippin_builds() {
    construct_from_xml_file("trippin.xml", ConstructConfig::default(), &Bump::new()).unwrap();
}

#[test]
fn enum_no_values_assigned_consecutively() {
    let arena = Bump::new();
    let model = construct_from_xml_file(
        "enum_no_explicit_values.xml",
        ConstructConfig::default(),
        &arena,
    )
    .unwrap();

    assert_eq!(1, model.schemas.len());
    assert_eq!(1, model.schemas[0].enum_types.len());
    assert_eq!(
        &EnumType {
            name: QualifiedName {
                qualifier: "Microsoft.OData.SampleService.Models.TripPin",
                uq_name: "PersonGender"
            },
            members: vec![
                EnumTypeMember {
                    name: "Male".to_owned(),
                    value: 0
                },
                EnumTypeMember {
                    name: "Female".to_owned(),
                    value: 1
                },
                EnumTypeMember {
                    name: "Unknown".to_owned(),
                    value: 2
                },
            ],
            is_flags: false,
            underlying_type: EnumUnderlyingType::Int32,
        },
        model.schemas[0].enum_types[0]
    )
}

/// Make sure we don't stack overflow if base types are referenced cyclically.
#[test]
fn inheritance_cycle_detected() {
    let timeout = Duration::from_secs(2);
    let edmx_filepath: PathBuf = [
        "src",
        "entity_model",
        "test",
        "inheritance_reference_cycle.xml",
    ]
    .iter()
    .collect();

    let edmx_str = fs::read_to_string(edmx_filepath).unwrap();
    let t_edmx = serde_xml_rs::from_str(&edmx_str).unwrap();

    match timeout_action(
        move || {
            let arena = Bump::new();
            let config = ConstructConfig::default();
            let entity_model_result =
                construct_entity_model(t_edmx, SERVICE_URL.to_owned(), &arena, config);

            if !entity_model_result.is_err() {
                println!("Fail: entity model construction should have errored");
                panic!();
            };
        },
        timeout,
    ) {
        Ok(_) => {}
        Err(RecvTimeoutError::Disconnected) => panic!("Construction failed with panic"),
        // Will most likely just stack overflow and fail to print this in fail case
        Err(RecvTimeoutError::Timeout) => panic!(
            "Construction did not complete within timeout ({:?})",
            timeout
        ),
    }
}

#[test]
fn entity_model_debug_output_terminates() {
    let timeout = Duration::from_secs(2);
    let edmx_filepath: PathBuf = ["src", "entity_model", "test", "type_reference_cycle.xml"]
        .iter()
        .collect();

    let edmx_str = fs::read_to_string(edmx_filepath).unwrap();
    let t_edmx = serde_xml_rs::from_str(&edmx_str).unwrap();

    match timeout_action(
        move || {
            let arena = Bump::new();
            let config = ConstructConfig::default();
            let entity_model =
                construct_entity_model(t_edmx, SERVICE_URL.to_owned(), &arena, config).unwrap();
            format!("{:?}", entity_model);
        },
        timeout,
    ) {
        Ok(_) => {}
        Err(RecvTimeoutError::Disconnected) => panic!("Debug output failed with panic"),
        // Will most likely just stack overflow and fail to print this in fail case
        Err(RecvTimeoutError::Timeout) => {
            panic!("Debug did not complete within timeout ({:?})", timeout)
        }
    }
}

#[test]
fn key_from_base_type_builds() {
    construct_from_xml_file(
        "inherit_abstract_type.xml",
        ConstructConfig::default(),
        &Bump::new(),
    )
    .unwrap();
}

#[test]
fn namespace_and_alias_references_builds() {
    construct_from_xml_file(
        "namespace_alias.xml",
        ConstructConfig::default(),
        &Bump::new(),
    )
    .unwrap();
}

#[test]
fn invalid_nav_prop_binding_ignored_builds() {
    construct_from_xml_file(
        "invalid_nav_prop_binding_target.xml",
        ConstructConfig {
            skip_invalid_nav_prop_bindings: true,
            ..ConstructConfig::default()
        },
        &Bump::new(),
    )
    .unwrap();
}

#[test]
fn entity_type_no_key_not_in_collection_builds() {
    construct_from_xml_file(
        "entity_type_no_key_not_in_collection.xml",
        ConstructConfig::default(),
        &Bump::new(),
    )
    .unwrap();
}

#[test]
fn entity_type_no_key_in_collection_nav_prop_errors() {
    let arena = Bump::new();
    let result = construct_from_xml_file(
        "entity_type_no_key_in_collection_nav_prop.xml",
        ConstructConfig::default(),
        &arena,
    );

    assert!(result.is_err(), "Construction should have failed");
}

#[test]
fn entity_type_no_key_in_entity_set_errors() {
    let arena = Bump::new();
    let result = construct_from_xml_file(
        "entity_type_no_key_in_entity_set.xml",
        ConstructConfig::default(),
        &arena,
    );

    assert!(result.is_err(), "Construction should have failed");
}

#[test]
fn overloaded_identical_key_builds() {
    construct_from_xml_file(
        "overloaded_identical_key.xml",
        ConstructConfig::default(),
        &Bump::new(),
    )
    .unwrap();
}

#[test]
fn overloaded_differing_key_errors() {
    let arena = Bump::new();
    let result = construct_from_xml_file(
        "overloaded_differing_key.xml",
        ConstructConfig::default(),
        &arena,
    );

    assert!(result.is_err(), "Construction should have failed");
}

fn construct_from_xml_file<'ar>(
    filename: &str,
    config: ConstructConfig,
    arena: &'ar Bump,
) -> Result<EntityModel<'ar>, anyhow::Error> {
    let edmx_filepath: PathBuf = ["src", "entity_model", "test", filename].iter().collect();

    let edmx_str = fs::read_to_string(edmx_filepath).unwrap();
    let t_edmx = serde_xml_rs::from_str(&edmx_str).unwrap();

    construct_entity_model(t_edmx, SERVICE_URL.to_owned(), &arena, config)
}

fn timeout_action<T: Send + 'static>(
    action: impl FnOnce() -> T + Send + 'static,
    timeout: Duration,
) -> Result<T, RecvTimeoutError> {
    let (sender, receiver) = mpsc::channel();

    thread::spawn(move || sender.send(action()));

    receiver.recv_timeout(timeout)
}