postman_collection 0.3.1

A Postman Collection serialization & deserialization library.
Documentation
use std::path::{Path, PathBuf};

use glob::glob;
#[cfg(feature = "yaml")]
use postman_collection::to_yaml;
use postman_collection::{Error, PostmanCollection, PostmanCollectionVersion, from_path, from_str};

fn collection_fixture_paths() -> Vec<PathBuf> {
    let pattern = format!(
        "{}/tests/fixtures/collection/*.json",
        env!("CARGO_MANIFEST_DIR")
    );
    let mut paths = glob(&pattern)
        .expect("Failed to read fixture glob pattern")
        .collect::<std::result::Result<Vec<_>, _>>()
        .expect("Failed to enumerate collection fixtures");
    paths.sort();
    paths
}

fn expected_version(path: &Path) -> PostmanCollectionVersion {
    let filename = path
        .file_name()
        .and_then(|name| name.to_str())
        .expect("Fixture filename should be valid UTF-8");

    if filename.contains("v1.0.0") {
        PostmanCollectionVersion::V1_0_0
    } else if filename.contains("v2.0.0") {
        PostmanCollectionVersion::V2_0_0
    } else if filename.contains("v2.1.0") {
        PostmanCollectionVersion::V2_1_0
    } else {
        panic!("Unexpected fixture filename: {filename}");
    }
}

#[test]
fn discovers_collection_fixtures() {
    let fixtures = collection_fixture_paths();
    assert!(
        !fixtures.is_empty(),
        "Expected at least one collection fixture under tests/fixtures/collection"
    );
}

#[test]
fn deserializes_collection_fixtures_with_expected_versions() {
    for path in collection_fixture_paths() {
        let collection = from_path(&path).expect("Fixture should deserialize");
        let expected = expected_version(&path);

        assert_eq!(
            collection.version(),
            expected,
            "Wrong collection version for {}",
            path.display()
        );

        match expected {
            PostmanCollectionVersion::V1_0_0 => {
                assert!(matches!(collection, PostmanCollection::V1_0_0(_)));
            }
            PostmanCollectionVersion::V2_0_0 => {
                assert!(matches!(collection, PostmanCollection::V2_0_0(_)));
            }
            PostmanCollectionVersion::V2_1_0 => {
                assert!(matches!(collection, PostmanCollection::V2_1_0(_)));
            }
        }
    }
}

#[test]
fn collection_fixtures_round_trip_without_losing_information() {
    for path in collection_fixture_paths() {
        let parsed = from_path(&path).expect("Fixture should deserialize");
        let json = postman_collection::to_json(&parsed).expect("Collection should serialize");
        let reparsed = from_str(&json).expect("Serialized JSON should deserialize");

        assert_eq!(reparsed, parsed, "Round-trip changed {}", path.display());
    }
}

#[cfg(feature = "yaml")]
#[test]
fn yaml_serialization_works_when_feature_is_enabled() {
    let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures/collection/swagger-petstore-v2.1.0.json");
    let original = from_path(&fixture).expect("Fixture should deserialize");
    let yaml = to_yaml(&original).expect("Collection should serialize to YAML");
    let reparsed = from_str(&yaml).expect("YAML output should deserialize");

    assert!(yaml.contains("info:"), "unexpected YAML output: {yaml}");
    assert!(
        yaml.contains("name: Swagger Petstore"),
        "unexpected YAML output: {yaml}"
    );
    assert!(yaml.contains("item:"), "unexpected YAML output: {yaml}");
    assert_eq!(reparsed.version(), PostmanCollectionVersion::V2_1_0);
    assert_eq!(reparsed.name(), original.name());
}

#[test]
fn schema_tagged_v2_collections_take_precedence_over_v1_shape_heuristics() {
    for (schema, expected) in [
        (
            "https://schema.getpostman.com/json/collection/v2.0.0/collection.json",
            PostmanCollectionVersion::V2_0_0,
        ),
        (
            "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
            PostmanCollectionVersion::V2_1_0,
        ),
    ] {
        let input = format!(
            r#"{{
                "info": {{
                    "name": "Example",
                    "schema": "{schema}"
                }},
                "item": [],
                "order": ["custom-metadata"]
            }}"#
        );

        let collection = from_str(&input).expect("Schema-tagged v2 collection should deserialize");
        assert_eq!(collection.version(), expected);

        match expected {
            PostmanCollectionVersion::V1_0_0 => unreachable!("Only v2 schemas are under test"),
            PostmanCollectionVersion::V2_0_0 => {
                assert!(matches!(collection, PostmanCollection::V2_0_0(_)));
            }
            PostmanCollectionVersion::V2_1_0 => {
                assert!(matches!(collection, PostmanCollection::V2_1_0(_)));
            }
        }
    }
}

#[test]
fn rejects_unknown_schema_versions_even_with_v1_shape_keys_present() {
    let input = r#"{
        "info": {
            "name": "Example",
            "schema": "https://schema.getpostman.com/json/collection/v9.9.9/collection.json"
        },
        "item": [],
        "order": ["custom-metadata"]
    }"#;

    let error = from_str(input).expect_err("Unknown schema should fail");
    assert!(matches!(error, Error::UnsupportedSpecFileVersion { .. }));
}