oas3-gen 0.25.0

A rust type generator for OpenAPI v3.1.x specification.
use std::collections::BTreeSet;

use quote::ToTokens as _;

use crate::generator::codegen::types::{ModuleUsesFragment, UseFragment};

fn set_from<const N: usize>(items: [&str; N]) -> BTreeSet<String> {
  items.into_iter().map(String::from).collect::<BTreeSet<_>>()
}

#[test]
fn empty_set_produces_no_output() {
  let fragment = ModuleUsesFragment::new(BTreeSet::new());
  let code = fragment.into_token_stream().to_string();
  assert!(code.is_empty(), "empty set should produce empty output, got: {code}");
}

#[test]
fn single_path_without_separator_is_skipped() {
  let fragment = ModuleUsesFragment::new(set_from(["standalone"]));
  let code = fragment.into_token_stream().to_string();
  assert!(code.is_empty(), "path without :: should be skipped, got: {code}");
}

#[test]
fn single_item_from_single_module() {
  let fragment = ModuleUsesFragment::new(set_from(["serde::Serialize"]));
  let code = fragment.into_token_stream().to_string();
  assert!(
    code.contains("use serde :: Serialize ;"),
    "single item should produce simple use statement, got: {code}"
  );
  assert!(!code.contains('{'), "single item should not use braces, got: {code}");
}

#[test]
fn multiple_items_from_same_module_grouped() {
  let fragment = ModuleUsesFragment::new(set_from(["serde::Deserialize", "serde::Serialize"]));
  let code = fragment.into_token_stream().to_string();
  assert!(
    code.contains("use serde :: { Deserialize , Serialize } ;"),
    "multiple items from same module should be grouped, got: {code}"
  );
}

#[test]
fn multiple_modules_produce_separate_statements() {
  let fragment = ModuleUsesFragment::new(set_from(["serde::Serialize", "std::collections::BTreeMap"]));
  let code = fragment.into_token_stream().to_string();
  assert!(
    code.contains("use serde :: Serialize ;"),
    "should have serde import, got: {code}"
  );
  assert!(
    code.contains("use std :: collections :: BTreeMap ;"),
    "should have std::collections import, got: {code}"
  );
}

#[test]
fn deeply_nested_module_paths() {
  let fragment = ModuleUsesFragment::new(set_from([
    "std::collections::BTreeMap",
    "std::collections::BTreeSet",
    "std::collections::HashMap",
  ]));
  let code = fragment.into_token_stream().to_string();
  assert!(
    code.contains("use std :: collections :: { BTreeMap , BTreeSet , HashMap } ;"),
    "deeply nested paths should group by full module path, got: {code}"
  );
}

#[test]
fn btreeset_ordering_deterministic() {
  let fragment = ModuleUsesFragment::new(set_from(["serde::Serialize", "anyhow::Result", "quote::ToTokens"]));
  let code = fragment.into_token_stream().to_string();

  let anyhow_pos = code.find("anyhow").expect("should contain anyhow");
  let quote_pos = code.find("quote").expect("should contain quote");
  let serde_pos = code.find("serde").expect("should contain serde");
  assert!(
    anyhow_pos < quote_pos && quote_pos < serde_pos,
    "imports should be in alphabetical order by module: anyhow < quote < serde, got: {code}"
  );
}

#[test]
fn mixed_module_depths() {
  let fragment = ModuleUsesFragment::new(set_from([
    "serde::Deserialize",
    "serde::Serialize",
    "std::collections::BTreeMap",
    "std::io::Write",
  ]));
  let code = fragment.into_token_stream().to_string();

  assert!(
    code.contains("use serde :: { Deserialize , Serialize } ;"),
    "serde items should be grouped, got: {code}"
  );
  assert!(
    code.contains("use std :: collections :: BTreeMap ;"),
    "collections should have single import, got: {code}"
  );
  assert!(
    code.contains("use std :: io :: Write ;"),
    "io should have single import, got: {code}"
  );
}

#[test]
fn use_fragment_single_item_no_braces() {
  let fragment = UseFragment::new("serde".to_string(), vec!["Serialize".to_string()]);
  let code = fragment.into_token_stream().to_string();
  assert!(
    code.contains("use serde :: Serialize ;"),
    "single item should not have braces, got: {code}"
  );
  assert!(!code.contains('{'), "should not contain opening brace, got: {code}");
}

#[test]
fn use_fragment_multiple_items_with_braces() {
  let fragment = UseFragment::new(
    "serde".to_string(),
    vec!["Deserialize".to_string(), "Serialize".to_string()],
  );
  let code = fragment.into_token_stream().to_string();
  assert!(
    code.contains("use serde :: { Deserialize , Serialize } ;"),
    "multiple items should use braces, got: {code}"
  );
}

#[test]
fn use_fragment_invalid_module_path_skipped() {
  let fragment = UseFragment::new("not a valid path".to_string(), vec!["Item".to_string()]);
  let code = fragment.into_token_stream().to_string();
  assert!(
    code.is_empty(),
    "invalid module path should produce no output, got: {code}"
  );
}

#[test]
fn use_fragment_invalid_item_filtered() {
  let fragment = UseFragment::new(
    "serde".to_string(),
    vec!["Serialize".to_string(), "not valid".to_string()],
  );
  let code = fragment.into_token_stream().to_string();
  assert!(
    code.contains("use serde :: Serialize ;"),
    "valid item should still be included, got: {code}"
  );
  assert!(
    !code.contains("not valid"),
    "invalid item should be filtered out, got: {code}"
  );
}

#[test]
fn items_within_module_alphabetically_sorted() {
  let fragment = ModuleUsesFragment::new(set_from([
    "serde::Serialize",
    "serde::Deserialize",
    "serde::de::Visitor",
  ]));
  let code = fragment.into_token_stream().to_string();

  assert!(
    code.contains("use serde :: { Deserialize , Serialize } ;"),
    "serde items should be grouped alphabetically, got: {code}"
  );
  assert!(
    code.contains("use serde :: de :: Visitor ;"),
    "serde::de should be separate module, got: {code}"
  );
}

#[test]
fn same_prefix_different_submodules_not_grouped() {
  let fragment = ModuleUsesFragment::new(set_from([
    "std::collections::BTreeMap",
    "std::io::Read",
    "std::io::Write",
  ]));
  let code = fragment.into_token_stream().to_string();

  assert!(
    code.contains("use std :: collections :: BTreeMap ;"),
    "collections should be single import, got: {code}"
  );
  assert!(
    code.contains("use std :: io :: { Read , Write } ;"),
    "io items should be grouped, got: {code}"
  );
}

#[test]
fn many_items_from_same_module_stress_test() {
  let fragment = ModuleUsesFragment::new(set_from([
    "std::collections::BTreeMap",
    "std::collections::BTreeSet",
    "std::collections::HashMap",
    "std::collections::HashSet",
    "std::collections::LinkedList",
    "std::collections::VecDeque",
  ]));
  let code = fragment.into_token_stream().to_string();

  assert!(
    code.contains("use std :: collections :: { BTreeMap , BTreeSet , HashMap , HashSet , LinkedList , VecDeque } ;"),
    "all collections items should be grouped in order, got: {code}"
  );
}

#[test]
fn interleaved_modules_maintain_separation() {
  let fragment = ModuleUsesFragment::new(set_from(["a::Z", "b::A", "a::A", "b::Z"]));
  let code = fragment.into_token_stream().to_string();

  assert!(
    code.contains("use a :: { A , Z } ;"),
    "module 'a' items should be grouped, got: {code}"
  );
  assert!(
    code.contains("use b :: { A , Z } ;"),
    "module 'b' items should be grouped, got: {code}"
  );

  let a_pos = code.find("use a").expect("should contain 'use a'");
  let b_pos = code.find("use b").expect("should contain 'use b'");
  assert!(a_pos < b_pos, "module 'a' should come before 'b', got: {code}");
}

#[test]
fn real_world_codegen_imports() {
  let fragment = ModuleUsesFragment::new(set_from([
    "proc_macro2::TokenStream",
    "quote::ToTokens",
    "quote::quote",
    "serde::Deserialize",
    "serde::Serialize",
    "std::collections::BTreeMap",
    "std::collections::BTreeSet",
    "validator::Validate",
  ]));
  let code = fragment.into_token_stream().to_string();

  let assertions = [
    ("use proc_macro2 :: TokenStream ;", "proc_macro2 single import"),
    ("use quote :: { ToTokens , quote } ;", "quote grouped imports"),
    ("use serde :: { Deserialize , Serialize } ;", "serde grouped imports"),
    (
      "use std :: collections :: { BTreeMap , BTreeSet } ;",
      "collections grouped imports",
    ),
    ("use validator :: Validate ;", "validator single import"),
  ];

  for (expected, description) in assertions {
    assert!(code.contains(expected), "{description} missing, got: {code}");
  }
}

#[test]
fn crate_relative_paths() {
  let fragment = ModuleUsesFragment::new(set_from([
    "crate::generator::ast::TypeRef",
    "crate::generator::ast::StructDef",
    "crate::utils::SchemaExt",
  ]));
  let code = fragment.into_token_stream().to_string();

  assert!(
    code.contains("use crate :: generator :: ast :: { StructDef , TypeRef } ;"),
    "crate-relative ast imports should be grouped, got: {code}"
  );
  assert!(
    code.contains("use crate :: utils :: SchemaExt ;"),
    "crate-relative utils import should work, got: {code}"
  );
}

#[test]
fn triple_colon_edge_case() {
  let fragment = ModuleUsesFragment::new(set_from(["a::b::c::d::E"]));
  let code = fragment.into_token_stream().to_string();
  assert!(
    code.contains("use a :: b :: c :: d :: E ;"),
    "deeply nested path should split on last ::, got: {code}"
  );
}

#[test]
fn empty_items_after_filtering_produces_empty_braces() {
  let fragment = UseFragment::new(
    "serde".to_string(),
    vec!["not valid".to_string(), "also not valid".to_string()],
  );
  let code = fragment.into_token_stream().to_string();
  assert!(
    code.contains("use serde :: { } ;"),
    "all-invalid items produce empty braces (edge case), got: {code}"
  );
}