use crate::checker::types::TypeId;
use crate::diagnostics::{DiagnosticDefinition, DiagnosticMap};
use crate::state_accessors::StateAccessors;
use std::collections::{HashMap, HashSet};
pub const VERSIONING_NAMESPACE: &str = "TypeSpec.Versioning";
pub const STATE_ADDED_ON: &str = "TypeSpec.Versioning.addedOn";
pub const STATE_REMOVED_ON: &str = "TypeSpec.Versioning.removedOn";
pub const STATE_VERSIONS: &str = "TypeSpec.Versioning.versions";
pub const STATE_USE_DEPENDENCY_NAMESPACE: &str = "TypeSpec.Versioning.useDependencyNamespace";
pub const STATE_USE_DEPENDENCY_ENUM: &str = "TypeSpec.Versioning.useDependencyEnum";
pub const STATE_RENAMED_FROM: &str = "TypeSpec.Versioning.renamedFrom";
pub const STATE_MADE_OPTIONAL: &str = "TypeSpec.Versioning.madeOptional";
pub const STATE_MADE_REQUIRED: &str = "TypeSpec.Versioning.madeRequired";
pub const STATE_TYPE_CHANGED_FROM: &str = "TypeSpec.Versioning.typeChangedFrom";
pub const STATE_RETURN_TYPE_CHANGED_FROM: &str = "TypeSpec.Versioning.returnTypeChangedFrom";
#[derive(Debug, Clone)]
pub struct Version {
pub name: String,
pub value: String,
pub index: usize,
}
#[derive(Debug, Clone)]
pub struct RenamedFromRecord {
pub version: Version,
pub old_name: String,
}
#[derive(Debug, Clone)]
pub struct VersionResolution {
pub root_version: Option<Version>,
pub versions: Vec<(TypeId, Version)>,
}
string_enum! {
pub enum Availability {
Unavailable => "Unavailable",
Added => "Added",
Available => "Available",
Removed => "Removed",
}
}
#[derive(Debug, Clone)]
pub struct VersionMap {
pub namespace: TypeId,
versions: Vec<Version>,
}
impl VersionMap {
pub fn new(namespace: TypeId, version_names: &[(&str, Option<&str>)]) -> Self {
let versions: Vec<Version> = version_names
.iter()
.enumerate()
.map(|(index, (name, value))| Version {
name: name.to_string(),
value: value.unwrap_or(name).to_string(),
index,
})
.collect();
Self {
namespace,
versions,
}
}
pub fn len(&self) -> usize {
self.versions.len()
}
pub fn is_empty(&self) -> bool {
self.versions.is_empty()
}
pub fn get_versions(&self) -> &[Version] {
&self.versions
}
pub fn get_by_index(&self, index: usize) -> Option<&Version> {
self.versions.get(index)
}
pub fn get_by_name(&self, name: &str) -> Option<&Version> {
self.versions.iter().find(|v| v.name == name)
}
}
pub const STATE_USE_DEPENDENCY: &str = "TypeSpec.Versioning.useDependency";
pub const STATE_VERSION_INDEX: &str = "TypeSpec.Versioning.versionIndex";
pub fn apply_use_dependency(state: &mut StateAccessors, target: TypeId, version_refs: &[TypeId]) {
let value: String = version_refs
.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(",");
state.set_state(STATE_USE_DEPENDENCY, target, value);
}
pub fn get_use_dependency(state: &StateAccessors, target: TypeId) -> Vec<TypeId> {
state
.get_state(STATE_USE_DEPENDENCY, target)
.map(|s| s.split(',').filter_map(|v| v.parse().ok()).collect())
.unwrap_or_default()
}
pub fn create_versioning_library() -> DiagnosticMap {
HashMap::from([
(
"versioned-dependency-tuple".to_string(),
DiagnosticDefinition::error(
"Versioned dependency mapping must be a tuple [SourceVersion, TargetVersion].",
),
),
(
"versioned-dependency-tuple-enum-member".to_string(),
DiagnosticDefinition::error(
"Versioned dependency mapping must be between enum members.",
),
),
(
"versioned-dependency-same-namespace".to_string(),
DiagnosticDefinition::error(
"Versioned dependency mapping must all point to the same namespace.",
),
),
(
"versioned-dependency-not-picked".to_string(),
DiagnosticDefinition::error(
"The versionedDependency decorator must provide a version of the dependency.",
),
),
(
"version-not-found".to_string(),
DiagnosticDefinition::error("The provided version is not declared as a version enum."),
),
(
"version-duplicate".to_string(),
DiagnosticDefinition::error(
"Multiple versions resolve to the same value. Version enums must resolve to unique values.",
),
),
(
"invalid-renamed-from-value".to_string(),
DiagnosticDefinition::error("@renamedFrom.oldName cannot be empty string."),
),
(
"incompatible-versioned-reference".to_string(),
DiagnosticDefinition::error_with_messages(vec![
(
"default",
"'{sourceName}' is referencing versioned type '{targetName}' but is not versioned itself.",
),
(
"addedAfter",
"'{sourceName}' was added in version '{sourceAddedOn}' but referencing type '{targetName}' added in version '{targetAddedOn}'.",
),
(
"dependentAddedAfter",
"'{sourceName}' was added in version '{sourceAddedOn}' but contains type '{targetName}' added in version '{targetAddedOn}'.",
),
(
"removedBefore",
"'{sourceName}' was removed in version '{sourceRemovedOn}' but referencing type '{targetName}' removed in version '{targetRemovedOn}'.",
),
(
"dependentRemovedBefore",
"'{sourceName}' was removed in version '{sourceRemovedOn}' but contains type '{targetName}' removed in version '{targetRemovedOn}'.",
),
(
"versionedDependencyAddedAfter",
"'{sourceName}' is referencing type '{targetName}' added in version '{targetAddedOn}' but version used is '{dependencyVersion}'.",
),
(
"versionedDependencyRemovedBefore",
"'{sourceName}' is referencing type '{targetName}' removed in version '{targetRemovedOn}' but version used is '{dependencyVersion}'.",
),
(
"doesNotExist",
"'{sourceName}' is referencing type '{targetName}' which does not exist in version '{version}'.",
),
]),
),
(
"incompatible-versioned-namespace-use-dependency".to_string(),
DiagnosticDefinition::error(
"The useDependency decorator can only be used on a Namespace if the namespace is unversioned. For versioned namespaces, put the useDependency decorator on the version enum members.",
),
),
(
"made-optional-not-optional".to_string(),
DiagnosticDefinition::error("Property marked with @madeOptional but is required."),
),
(
"made-required-optional".to_string(),
DiagnosticDefinition::error("Property marked with @madeRequired but is optional."),
),
(
"renamed-duplicate-property".to_string(),
DiagnosticDefinition::error(
"Property marked with '@renamedFrom' conflicts with existing property.",
),
),
])
}
fn apply_version_index(
state: &mut StateAccessors,
state_key: &str,
target: TypeId,
version_index: usize,
) {
let existing = state.get_state(state_key, target).unwrap_or("").to_string();
let new_value = if existing.is_empty() {
version_index.to_string()
} else {
let mut indices: Vec<usize> = existing.split(',').filter_map(|s| s.parse().ok()).collect();
if !indices.contains(&version_index) {
indices.push(version_index);
}
indices.sort();
indices
.iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(",")
};
state.set_state(state_key, target, new_value);
}
pub fn apply_added(state: &mut StateAccessors, target: TypeId, version_index: usize) {
apply_version_index(state, STATE_ADDED_ON, target, version_index);
}
pub fn get_added_on_versions(state: &StateAccessors, target: TypeId) -> Vec<usize> {
state
.get_state(STATE_ADDED_ON, target)
.map(|s| s.split(',').filter_map(|v| v.parse().ok()).collect())
.unwrap_or_default()
}
pub fn apply_removed(state: &mut StateAccessors, target: TypeId, version_index: usize) {
apply_version_index(state, STATE_REMOVED_ON, target, version_index);
}
pub fn get_removed_on_versions(state: &StateAccessors, target: TypeId) -> Vec<usize> {
state
.get_state(STATE_REMOVED_ON, target)
.map(|s| s.split(',').filter_map(|v| v.parse().ok()).collect())
.unwrap_or_default()
}
pub fn apply_versioned(state: &mut StateAccessors, target: TypeId, versions_enum_name: &str) {
state.set_state(STATE_VERSIONS, target, versions_enum_name.to_string());
}
pub fn is_versioned(state: &StateAccessors, target: TypeId) -> bool {
state.get_state(STATE_VERSIONS, target).is_some()
}
pub fn get_version_enum_name(state: &StateAccessors, target: TypeId) -> Option<String> {
state
.get_state(STATE_VERSIONS, target)
.map(|s| s.to_string())
}
fn apply_indexed_entry(
state: &mut StateAccessors,
state_key: &str,
target: TypeId,
version_index: usize,
name: &str,
) {
let existing = state.get_state(state_key, target).unwrap_or("").to_string();
let entry = format!("{}::{}", version_index, name);
let new_value = if existing.is_empty() {
entry
} else {
format!("{}||{}", existing, entry)
};
state.set_state(state_key, target, new_value);
}
fn get_indexed_entries(
state: &StateAccessors,
state_key: &str,
target: TypeId,
) -> Vec<(usize, String)> {
state
.get_state(state_key, target)
.map(|s| {
s.split("||")
.filter_map(|entry| {
let parts: Vec<&str> = entry.splitn(2, "::").collect();
if parts.len() == 2 {
Some((parts[0].parse().ok()?, parts[1].to_string()))
} else {
None
}
})
.collect()
})
.unwrap_or_default()
}
pub fn apply_renamed_from(
state: &mut StateAccessors,
target: TypeId,
version_index: usize,
old_name: &str,
) {
apply_indexed_entry(state, STATE_RENAMED_FROM, target, version_index, old_name);
}
pub fn get_renamed_from(state: &StateAccessors, target: TypeId) -> Vec<(usize, String)> {
get_indexed_entries(state, STATE_RENAMED_FROM, target)
}
numeric_decorator!(
apply_made_optional,
get_made_optional_on,
STATE_MADE_OPTIONAL,
usize
);
numeric_decorator!(
apply_made_required,
get_made_required_on,
STATE_MADE_REQUIRED,
usize
);
pub fn apply_type_changed_from(
state: &mut StateAccessors,
target: TypeId,
version_index: usize,
old_type_name: &str,
) {
apply_indexed_entry(
state,
STATE_TYPE_CHANGED_FROM,
target,
version_index,
old_type_name,
);
}
pub fn get_type_changed_from(state: &StateAccessors, target: TypeId) -> Vec<(usize, String)> {
get_indexed_entries(state, STATE_TYPE_CHANGED_FROM, target)
}
pub fn apply_return_type_changed_from(
state: &mut StateAccessors,
target: TypeId,
version_index: usize,
old_type_name: &str,
) {
apply_indexed_entry(
state,
STATE_RETURN_TYPE_CHANGED_FROM,
target,
version_index,
old_type_name,
);
}
pub fn get_return_type_changed_from(
state: &StateAccessors,
target: TypeId,
) -> Vec<(usize, String)> {
get_indexed_entries(state, STATE_RETURN_TYPE_CHANGED_FROM, target)
}
pub fn find_versioned_namespace<F>(
state: &StateAccessors,
start: TypeId,
parent_resolver: F,
) -> Option<TypeId>
where
F: Fn(TypeId) -> Option<TypeId>,
{
let mut current = Some(start);
while let Some(ns) = current {
if is_versioned(state, ns) {
return Some(ns);
}
current = parent_resolver(ns);
}
None
}
pub fn get_renamed_from_versions(state: &StateAccessors, target: TypeId) -> Vec<usize> {
get_renamed_from(state, target)
.iter()
.map(|(idx, _)| *idx)
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VersioningMutatorKind {
Versioned,
Transient,
}
#[derive(Debug, Clone)]
pub struct VersionSnapshotMutation {
pub version: Version,
pub name: String,
}
#[derive(Debug, Clone)]
pub struct VersionedMutators {
pub snapshots: Vec<VersionSnapshotMutation>,
}
#[derive(Debug, Clone)]
pub struct TransientVersioningMutator {
pub name: String,
}
#[derive(Debug, Clone)]
pub enum VersioningMutators {
Versioned(VersionedMutators),
Transient(TransientVersioningMutator),
}
#[derive(Debug, Clone)]
pub struct TimelineMoment {
version_map: Vec<(TypeId, Version)>,
pub name: String,
}
impl TimelineMoment {
pub fn new(version_map: Vec<(TypeId, Version)>) -> Self {
let name = version_map
.first()
.map(|(_, v)| v.name.clone())
.unwrap_or_default();
Self { version_map, name }
}
pub fn get_version(&self, namespace: TypeId) -> Option<&Version> {
self.version_map
.iter()
.find(|(ns, _)| *ns == namespace)
.map(|(_, v)| v)
}
pub fn versions(&self) -> impl Iterator<Item = &Version> {
self.version_map.iter().map(|(_, v)| v)
}
pub fn entries(&self) -> &[(TypeId, Version)] {
&self.version_map
}
}
#[derive(Debug, Clone)]
pub struct VersioningTimeline {
namespaces: Vec<TypeId>,
timeline: Vec<TimelineMoment>,
version_index: HashMap<(TypeId, usize), usize>,
}
impl VersioningTimeline {
pub fn new<F>(resolutions: &[Vec<(TypeId, Version)>], all_versions_fn: F) -> Self
where
F: Fn(TypeId) -> Option<Vec<Version>>,
{
let mut indexed_versions: HashSet<(TypeId, usize)> = HashSet::new();
let mut namespaces_set: HashSet<TypeId> = HashSet::new();
let mut timeline: Vec<TimelineMoment> = resolutions
.iter()
.map(|resolution| TimelineMoment::new(resolution.clone()))
.collect();
for resolution in resolutions {
for &(ns, ref ver) in resolution {
indexed_versions.insert((ns, ver.index));
namespaces_set.insert(ns);
}
}
let namespaces: Vec<TypeId> = namespaces_set.into_iter().collect();
for &ns in &namespaces {
if let Some(versions) = all_versions_fn(ns) {
for ver in versions {
if !indexed_versions.contains(&(ns, ver.index)) {
indexed_versions.insert((ns, ver.index));
let insert_index = Self::find_index_to_insert(&timeline, ns, &ver);
let new_moment = TimelineMoment::new(vec![(ns, ver)]);
if let Some(idx) = insert_index {
timeline.insert(idx, new_moment);
} else {
timeline.push(new_moment);
}
}
}
}
}
let mut version_index: HashMap<(TypeId, usize), usize> = HashMap::new();
for (idx, moment) in timeline.iter().enumerate() {
for (ns, ver) in &moment.version_map {
version_index.entry((*ns, ver.index)).or_insert(idx);
}
}
Self {
namespaces,
timeline,
version_index,
}
}
fn find_index_to_insert(
timeline: &[TimelineMoment],
namespace: TypeId,
version: &Version,
) -> Option<usize> {
for (index, moment) in timeline.iter().enumerate() {
if let Some(ver_at_moment) = moment.get_version(namespace)
&& version.index < ver_at_moment.index
{
return Some(index);
}
}
None
}
pub fn get_moment(&self, index: usize) -> Option<&TimelineMoment> {
self.timeline.get(index)
}
pub fn get_moment_for_version(
&self,
namespace: TypeId,
version_index: usize,
) -> Option<&TimelineMoment> {
self.version_index
.get(&(namespace, version_index))
.and_then(|&idx| self.timeline.get(idx))
}
pub fn get_index(&self, namespace: TypeId, version_index: usize) -> Option<usize> {
self.version_index.get(&(namespace, version_index)).copied()
}
pub fn is_before(&self, ns_a: TypeId, ver_a: usize, ns_b: TypeId, ver_b: usize) -> bool {
let idx_a = self.get_index(ns_a, ver_a);
let idx_b = self.get_index(ns_b, ver_b);
match (idx_a, idx_b) {
(Some(a), Some(b)) => a < b,
_ => false,
}
}
pub fn first(&self) -> Option<&TimelineMoment> {
self.timeline.first()
}
pub fn len(&self) -> usize {
self.timeline.len()
}
pub fn is_empty(&self) -> bool {
self.timeline.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (usize, &TimelineMoment)> {
self.timeline.iter().enumerate()
}
pub fn namespaces(&self) -> &[TypeId] {
&self.namespaces
}
}
pub fn resolve_when_first_added(
added: &[Version],
removed: &[Version],
parent_added: &Version,
) -> Vec<Version> {
let implicitly_available = added.is_empty() && removed.is_empty();
if implicitly_available {
return vec![parent_added.clone()];
}
if !added.is_empty() {
let added_first = removed.is_empty() || added[0].index < removed[0].index;
if added_first {
return added.to_vec();
}
}
if !removed.is_empty() {
let removed_first = added.is_empty() || removed[0].index < added[0].index;
if removed_first {
let mut result = vec![parent_added.clone()];
result.extend(added.iter().cloned());
return result;
}
}
added.to_vec()
}
pub fn resolve_removed(
added: &[Version],
removed: &[Version],
parent_removed: Option<&Version>,
) -> Vec<Version> {
if !removed.is_empty() {
return removed.to_vec();
}
let implicitly_removed =
added.is_empty() || parent_removed.is_some_and(|pr| added[0].index < pr.index);
if let Some(pr) = parent_removed
&& implicitly_removed
{
return vec![pr.clone()];
}
Vec::new()
}
pub fn get_availability_map(
all_versions: &[Version],
added: &[Version],
removed: &[Version],
parent_added: &Version,
parent_removed: Option<&Version>,
has_type_changed: bool,
has_return_type_changed: bool,
) -> Option<Vec<(usize, Availability)>> {
if added.is_empty() && removed.is_empty() && !has_type_changed && !has_return_type_changed {
return None;
}
let resolved_added = resolve_when_first_added(added, removed, parent_added);
let resolved_removed = resolve_removed(added, removed, parent_removed);
let mut result = Vec::new();
let mut is_avail = false;
for ver in all_versions {
let is_added = resolved_added.iter().any(|a| a.index == ver.index);
let is_removed = resolved_removed.iter().any(|r| r.index == ver.index);
if is_removed {
is_avail = false;
result.push((ver.index, Availability::Removed));
} else if is_added {
is_avail = true;
result.push((ver.index, Availability::Added));
} else if is_avail {
result.push((ver.index, Availability::Available));
} else {
result.push((ver.index, Availability::Unavailable));
}
}
Some(result)
}
pub const VERSIONING_DECORATORS_TSP: &str = r#"
import "../../dist/src/lib/tsp-index.js";
namespace TypeSpec.Versioning;
extern dec versioned(target: Namespace, versions: Enum);
extern dec added(target: Model | ModelProperty | Operation | Enum | EnumMember | Union | UnionVariant | Scalar | Interface, v: EnumMember);
extern dec removed(target: Model | ModelProperty | Operation | Enum | EnumMember | Union | UnionVariant | Scalar | Interface, v: EnumMember);
extern dec renamedFrom(target: Model | ModelProperty | Operation | Enum | EnumMember | Union | UnionVariant | Scalar | Interface, v: EnumMember, oldName: valueof string);
extern dec madeOptional(target: ModelProperty, v: EnumMember);
extern dec madeRequired(target: ModelProperty, v: EnumMember);
extern dec typeChangedFrom(target: ModelProperty, v: EnumMember, oldType: unknown);
extern dec returnTypeChangedFrom(target: Operation, v: EnumMember, oldReturnType: unknown);
extern dec useDependency(target: EnumMember | Namespace, ...versionRecords: EnumMember[]);
"#;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_namespace() {
assert_eq!(VERSIONING_NAMESPACE, "TypeSpec.Versioning");
}
#[test]
fn test_create_versioning_library() {
let diags = create_versioning_library();
assert!(diags.len() >= 12);
}
#[test]
fn test_apply_added() {
let mut state = StateAccessors::new();
assert!(get_added_on_versions(&state, 1).is_empty());
apply_added(&mut state, 1, 2);
assert_eq!(get_added_on_versions(&state, 1), vec![2]);
}
#[test]
fn test_apply_added_multiple_versions() {
let mut state = StateAccessors::new();
apply_added(&mut state, 1, 3);
apply_added(&mut state, 1, 1);
assert_eq!(get_added_on_versions(&state, 1), vec![1, 3]);
}
#[test]
fn test_apply_removed() {
let mut state = StateAccessors::new();
apply_removed(&mut state, 1, 5);
assert_eq!(get_removed_on_versions(&state, 1), vec![5]);
}
#[test]
fn test_apply_versioned() {
let mut state = StateAccessors::new();
assert!(!is_versioned(&state, 1));
apply_versioned(&mut state, 1, "ApiVersion");
assert!(is_versioned(&state, 1));
assert_eq!(
get_version_enum_name(&state, 1),
Some("ApiVersion".to_string())
);
}
#[test]
fn test_apply_renamed_from() {
let mut state = StateAccessors::new();
apply_renamed_from(&mut state, 1, 2, "oldName");
let records = get_renamed_from(&state, 1);
assert_eq!(records.len(), 1);
assert_eq!(records[0], (2, "oldName".to_string()));
}
#[test]
fn test_apply_renamed_from_multiple() {
let mut state = StateAccessors::new();
apply_renamed_from(&mut state, 1, 1, "firstOld");
apply_renamed_from(&mut state, 1, 3, "secondOld");
let records = get_renamed_from(&state, 1);
assert_eq!(records.len(), 2);
}
#[test]
fn test_apply_made_optional() {
let mut state = StateAccessors::new();
assert_eq!(get_made_optional_on(&state, 1), None);
apply_made_optional(&mut state, 1, 3);
assert_eq!(get_made_optional_on(&state, 1), Some(3));
}
#[test]
fn test_apply_made_required() {
let mut state = StateAccessors::new();
apply_made_required(&mut state, 1, 2);
assert_eq!(get_made_required_on(&state, 1), Some(2));
}
#[test]
fn test_apply_type_changed_from() {
let mut state = StateAccessors::new();
apply_type_changed_from(&mut state, 1, 2, "string");
let records = get_type_changed_from(&state, 1);
assert_eq!(records.len(), 1);
assert_eq!(records[0], (2, "string".to_string()));
}
#[test]
fn test_apply_return_type_changed_from() {
let mut state = StateAccessors::new();
apply_return_type_changed_from(&mut state, 1, 3, "OldModel");
let records = get_return_type_changed_from(&state, 1);
assert_eq!(records.len(), 1);
assert_eq!(records[0], (3, "OldModel".to_string()));
}
#[test]
fn test_decorators_tsp_not_empty() {
assert!(!VERSIONING_DECORATORS_TSP.is_empty());
assert!(VERSIONING_DECORATORS_TSP.contains("dec versioned"));
assert!(VERSIONING_DECORATORS_TSP.contains("dec added"));
assert!(VERSIONING_DECORATORS_TSP.contains("dec removed"));
assert!(VERSIONING_DECORATORS_TSP.contains("dec renamedFrom"));
assert!(VERSIONING_DECORATORS_TSP.contains("dec madeOptional"));
assert!(VERSIONING_DECORATORS_TSP.contains("dec useDependency"));
}
#[test]
fn test_availability_enum() {
assert_eq!(Availability::Unavailable.as_str(), "Unavailable");
assert_eq!(Availability::Added.as_str(), "Added");
assert_eq!(Availability::Available.as_str(), "Available");
assert_eq!(Availability::Removed.as_str(), "Removed");
assert_eq!(Availability::parse_str("Added"), Some(Availability::Added));
assert_eq!(Availability::parse_str("unknown"), None);
}
#[test]
fn test_use_dependency() {
let mut state = StateAccessors::new();
assert!(get_use_dependency(&state, 1).is_empty());
apply_use_dependency(&mut state, 1, &[10, 20]);
let deps = get_use_dependency(&state, 1);
assert_eq!(deps, vec![10, 20]);
}
#[test]
fn test_version_map() {
let map = VersionMap::new(1, &[("v1", None), ("v2", Some("2024-01-01")), ("v3", None)]);
assert_eq!(map.len(), 3);
let versions = map.get_versions();
assert_eq!(versions[0].name, "v1");
assert_eq!(versions[0].value, "v1");
assert_eq!(versions[0].index, 0);
assert_eq!(versions[1].value, "2024-01-01");
assert_eq!(versions[1].index, 1);
assert_eq!(versions[2].index, 2);
}
#[test]
fn test_version_map_lookup() {
let map = VersionMap::new(1, &[("v1", None), ("v2", None)]);
assert_eq!(map.get_by_index(0).unwrap().name, "v1");
assert_eq!(map.get_by_index(1).unwrap().name, "v2");
assert!(map.get_by_index(2).is_none());
assert_eq!(map.get_by_name("v2").unwrap().index, 1);
assert!(map.get_by_name("v99").is_none());
}
#[test]
fn test_version_map_empty() {
let map = VersionMap::new(1, &[]);
assert!(map.is_empty());
assert_eq!(map.len(), 0);
}
#[test]
fn test_find_versioned_namespace() {
let mut state = StateAccessors::new();
apply_versioned(&mut state, 1, "ApiVersion");
let result = find_versioned_namespace(&state, 1, |_| None);
assert_eq!(result, Some(1));
let result = find_versioned_namespace(&state, 2, |id| if id == 2 { Some(1) } else { None });
assert_eq!(result, Some(1));
let result = find_versioned_namespace(&state, 99, |_| None);
assert_eq!(result, None);
}
#[test]
fn test_get_renamed_from_versions() {
let mut state = StateAccessors::new();
apply_renamed_from(&mut state, 1, 2, "oldName1");
apply_renamed_from(&mut state, 1, 5, "oldName2");
let versions = get_renamed_from_versions(&state, 1);
assert_eq!(versions, vec![2, 5]);
}
fn make_version(name: &str, index: usize) -> Version {
Version {
name: name.to_string(),
value: name.to_string(),
index,
}
}
#[test]
fn test_timeline_moment_basic() {
let moment =
TimelineMoment::new(vec![(1, make_version("v1", 0)), (2, make_version("l1", 0))]);
assert_eq!(moment.name, "v1");
assert!(moment.get_version(1).is_some());
assert_eq!(moment.get_version(1).unwrap().name, "v1");
assert!(moment.get_version(2).is_some());
assert_eq!(moment.get_version(2).unwrap().name, "l1");
assert!(moment.get_version(99).is_none());
}
#[test]
fn test_timeline_moment_versions() {
let moment =
TimelineMoment::new(vec![(1, make_version("v1", 0)), (2, make_version("l1", 0))]);
let versions: Vec<&Version> = moment.versions().collect();
assert_eq!(versions.len(), 2);
}
#[test]
fn test_versioning_timeline_basic() {
let resolutions = vec![
vec![(1, make_version("v1", 0))],
vec![(1, make_version("v2", 1))],
];
let timeline = VersioningTimeline::new(&resolutions, |_| None);
assert_eq!(timeline.len(), 2);
assert!(timeline.first().is_some());
assert_eq!(timeline.first().unwrap().name, "v1");
}
#[test]
fn test_versioning_timeline_with_library() {
let service_ns: TypeId = 1;
let library_ns: TypeId = 2;
let resolutions = vec![
vec![
(service_ns, make_version("v1", 0)),
(library_ns, make_version("l1", 0)),
],
vec![
(service_ns, make_version("v2", 1)),
(library_ns, make_version("l3", 2)),
],
];
let timeline = VersioningTimeline::new(&resolutions, |ns| {
if ns == library_ns {
Some(vec![
make_version("l1", 0),
make_version("l2", 1),
make_version("l3", 2),
make_version("l4", 3),
])
} else if ns == service_ns {
Some(vec![make_version("v1", 0), make_version("v2", 1)])
} else {
None
}
});
assert!(timeline.len() >= 2);
assert_eq!(timeline.get_index(service_ns, 0), Some(0));
assert!(timeline.is_before(service_ns, 0, service_ns, 1)); assert!(!timeline.is_before(service_ns, 1, service_ns, 0)); }
#[test]
fn test_versioning_timeline_get_moment_for_version() {
let ns: TypeId = 1;
let resolutions = vec![
vec![(ns, make_version("v1", 0))],
vec![(ns, make_version("v2", 1))],
];
let timeline = VersioningTimeline::new(&resolutions, |_| None);
let moment = timeline.get_moment_for_version(ns, 0);
assert!(moment.is_some());
assert_eq!(moment.unwrap().name, "v1");
let moment2 = timeline.get_moment_for_version(ns, 1);
assert!(moment2.is_some());
assert_eq!(moment2.unwrap().name, "v2");
assert!(timeline.get_moment_for_version(ns, 99).is_none());
}
#[test]
fn test_versioning_timeline_empty() {
let timeline = VersioningTimeline::new(&[], |_| None);
assert!(timeline.is_empty());
assert_eq!(timeline.len(), 0);
assert!(timeline.first().is_none());
}
#[test]
fn test_resolve_when_first_added_implicit() {
let parent = make_version("v1", 0);
let result = resolve_when_first_added(&[], &[], &parent);
assert_eq!(result.len(), 1);
assert_eq!(result[0].index, 0);
}
#[test]
fn test_resolve_when_first_added_explicit_first() {
let parent = make_version("v1", 0);
let added = vec![make_version("v2", 1)];
let result = resolve_when_first_added(&added, &[], &parent);
assert_eq!(result.len(), 1);
assert_eq!(result[0].index, 1);
}
#[test]
fn test_resolve_when_first_added_removed_first() {
let parent = make_version("v1", 0);
let added = vec![make_version("v3", 2)];
let removed = vec![make_version("v2", 1)];
let result = resolve_when_first_added(&added, &removed, &parent);
assert_eq!(result.len(), 2);
assert_eq!(result[0].index, 0); assert_eq!(result[1].index, 2); }
#[test]
fn test_resolve_removed_explicit() {
let added = vec![make_version("v1", 0)];
let removed = vec![make_version("v3", 2)];
let result = resolve_removed(&added, &removed, None);
assert_eq!(result.len(), 1);
assert_eq!(result[0].index, 2);
}
#[test]
fn test_resolve_removed_implicit_from_parent() {
let added = vec![make_version("v1", 0)];
let parent_removed = make_version("v2", 1);
let result = resolve_removed(&added, &[], Some(&parent_removed));
assert_eq!(result.len(), 1);
assert_eq!(result[0].index, 1);
}
#[test]
fn test_resolve_removed_not_implicit() {
let added = vec![make_version("v3", 2)];
let parent_removed = make_version("v1", 0);
let result = resolve_removed(&added, &[], Some(&parent_removed));
assert!(result.is_empty());
}
#[test]
fn test_get_availability_map_no_info() {
let all = vec![make_version("v1", 0), make_version("v2", 1)];
let parent = make_version("v1", 0);
let result = get_availability_map(&all, &[], &[], &parent, None, false, false);
assert!(result.is_none());
}
#[test]
fn test_get_availability_map_added_in_v2() {
let all = vec![
make_version("v1", 0),
make_version("v2", 1),
make_version("v3", 2),
];
let parent = make_version("v1", 0);
let added = vec![make_version("v2", 1)];
let result = get_availability_map(&all, &added, &[], &parent, None, false, false);
assert!(result.is_some());
let map = result.unwrap();
assert_eq!(map[0], (0, Availability::Unavailable)); assert_eq!(map[1], (1, Availability::Added)); assert_eq!(map[2], (2, Availability::Available)); }
#[test]
fn test_get_availability_map_added_and_removed() {
let all = vec![
make_version("v1", 0),
make_version("v2", 1),
make_version("v3", 2),
];
let parent = make_version("v1", 0);
let added = vec![make_version("v1", 0)];
let removed = vec![make_version("v2", 1)];
let result = get_availability_map(&all, &added, &removed, &parent, None, false, false);
assert!(result.is_some());
let map = result.unwrap();
assert_eq!(map[0], (0, Availability::Added)); assert_eq!(map[1], (1, Availability::Removed)); assert_eq!(map[2], (2, Availability::Unavailable)); }
#[test]
fn test_get_availability_map_with_type_changed() {
let all = vec![make_version("v1", 0), make_version("v2", 1)];
let parent = make_version("v1", 0);
let result = get_availability_map(&all, &[], &[], &parent, None, true, false);
assert!(result.is_some());
let map = result.unwrap();
assert_eq!(map[0], (0, Availability::Added)); assert_eq!(map[1], (1, Availability::Available)); }
#[test]
fn test_versioning_mutator_types() {
let versioned = VersioningMutators::Versioned(VersionedMutators {
snapshots: vec![
VersionSnapshotMutation {
version: make_version("v1", 0),
name: "VersionSnapshot v1".to_string(),
},
VersionSnapshotMutation {
version: make_version("v2", 1),
name: "VersionSnapshot v2".to_string(),
},
],
});
match versioned {
VersioningMutators::Versioned(v) => {
assert_eq!(v.snapshots.len(), 2);
assert_eq!(v.snapshots[0].version.name, "v1");
}
_ => panic!("Expected Versioned variant"),
}
let transient = VersioningMutators::Transient(TransientVersioningMutator {
name: "TransientVersionSnapshot".to_string(),
});
match transient {
VersioningMutators::Transient(t) => {
assert_eq!(t.name, "TransientVersionSnapshot");
}
_ => panic!("Expected Transient variant"),
}
}
#[test]
fn test_versioning_mutator_kind() {
assert_eq!(
VersioningMutatorKind::Versioned,
VersioningMutatorKind::Versioned
);
assert_ne!(
VersioningMutatorKind::Versioned,
VersioningMutatorKind::Transient
);
}
}