use std::collections::{BTreeMap, BTreeSet};
use serde::Serialize;
use crate::model::{Node, Pool, Profile, Strength, is_variant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Info,
Warning,
Breaking,
}
impl Severity {
pub fn as_str(self) -> &'static str {
match self {
Self::Info => "info",
Self::Warning => "warning",
Self::Breaking => "breaking",
}
}
}
pub const RULES: &[(&str, Severity, &str)] = &[
(
"profile-removed",
Severity::Breaking,
"A profile or extension definition was removed",
),
(
"profile-added",
Severity::Info,
"A profile or extension definition was added",
),
(
"profile-replaced",
Severity::Breaking,
"A profile gave way to one with another canonical URL: each is the only one of its type removed or added",
),
(
"base-changed",
Severity::Warning,
"The profile is based on a different definition",
),
(
"min-increased",
Severity::Breaking,
"The minimum cardinality went up",
),
(
"min-decreased",
Severity::Info,
"The minimum cardinality went down",
),
(
"max-decreased",
Severity::Breaking,
"The maximum cardinality went down",
),
(
"max-increased",
Severity::Info,
"The maximum cardinality went up",
),
(
"type-removed",
Severity::Breaking,
"A type the element allowed is no longer allowed",
),
(
"type-added",
Severity::Info,
"The element allows another type",
),
(
"target-removed",
Severity::Breaking,
"A reference target type is no longer allowed",
),
(
"target-narrowed",
Severity::Warning,
"A reference target was narrowed to a profile of the same type",
),
(
"target-added",
Severity::Info,
"A reference may point to another profile",
),
(
"must-support-added",
Severity::Breaking,
"The element became mustSupport",
),
(
"must-support-removed",
Severity::Info,
"The element is no longer mustSupport",
),
(
"binding-stronger",
Severity::Breaking,
"The binding became extensible or required, or required from extensible",
),
(
"binding-stronger-advisory",
Severity::Info,
"The binding strength went up but stays advisory (example → preferred)",
),
(
"binding-weaker",
Severity::Info,
"The binding strength went down",
),
(
"binding-added",
Severity::Breaking,
"A required or extensible binding was added",
),
(
"value-set-changed",
Severity::Warning,
"A required or extensible binding names another value set",
),
(
"invariant-added",
Severity::Breaking,
"An error invariant was added",
),
(
"invariant-warning-added",
Severity::Warning,
"A warning invariant was added",
),
(
"invariant-changed",
Severity::Warning,
"An invariant's expression changed",
),
(
"invariant-moved",
Severity::Info,
"An invariant moved to another element and says the same from there",
),
(
"invariant-removed",
Severity::Info,
"An invariant was removed",
),
(
"fixed-added",
Severity::Breaking,
"A fixed or pattern value was added",
),
(
"fixed-changed",
Severity::Breaking,
"The fixed or pattern value changed",
),
(
"fixed-removed",
Severity::Info,
"The fixed or pattern value was removed",
),
(
"slicing-closed",
Severity::Breaking,
"Slicing became closed",
),
("slicing-opened", Severity::Info, "Slicing became open"),
("slice-added", Severity::Info, "An optional slice was added"),
("slice-removed", Severity::Warning, "A slice was removed"),
(
"modifier-changed",
Severity::Warning,
"The isModifier flag changed",
),
];
pub fn severity_of(rule: &str) -> Severity {
RULES
.iter()
.find(|(id, _, _)| *id == rule)
.map_or(Severity::Info, |(_, severity, _)| *severity)
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Change {
pub rule: &'static str,
pub severity: Severity,
pub path: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub old: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
Added,
Removed,
Changed,
Unchanged,
}
#[derive(Debug, Clone, Serialize)]
pub struct ProfileDiff {
pub url: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub type_name: String,
pub status: Status,
#[serde(skip_serializing_if = "Option::is_none")]
pub replaces: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub old_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_version: Option<String>,
pub file: String,
pub changes: Vec<Change>,
}
impl ProfileDiff {
pub fn worst(&self) -> Option<Severity> {
self.changes.iter().map(|c| c.severity).max()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Report {
pub old: String,
pub new: String,
pub profiles: Vec<ProfileDiff>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub notes: Vec<String>,
}
impl Report {
pub fn count(&self, severity: Severity) -> usize {
self.profiles
.iter()
.flat_map(|p| &p.changes)
.filter(|c| c.severity == severity)
.count()
}
pub fn worst(&self) -> Option<Severity> {
self.profiles.iter().filter_map(ProfileDiff::worst).max()
}
}
#[derive(Debug, Clone, Default)]
pub struct Filter {
pub profiles: Vec<String>,
}
impl Filter {
fn admits(&self, profile: &Profile) -> bool {
self.profiles.is_empty()
|| self
.profiles
.iter()
.any(|p| p == &profile.url || p == &profile.name)
}
}
pub fn diff(old_id: &str, old: &Pool, new_id: &str, new: &Pool, filter: &Filter) -> Report {
let (old_all, new_all) = (old.profiles(), new.profiles());
let chosen: BTreeSet<String> = old_all
.iter()
.chain(&new_all)
.filter(|p| filter.admits(p))
.map(|p| p.url.clone())
.collect();
let keep = |profiles: Vec<Profile>| -> BTreeMap<String, Profile> {
profiles
.into_iter()
.filter(|p| chosen.contains(&p.url))
.map(|p| (p.url.clone(), p))
.collect()
};
let (old_profiles, new_profiles) = (keep(old_all), keep(new_all));
let urls: BTreeSet<&String> = old_profiles.keys().chain(new_profiles.keys()).collect();
let replacements = replacements(&old_profiles, &new_profiles);
let replaced: BTreeSet<&String> = replacements.values().collect();
let mut profiles = Vec::new();
for url in urls {
if replaced.contains(url) {
continue;
}
let predecessor = replacements
.get(url)
.and_then(|previous| old_profiles.get(previous));
let entry = match (predecessor.or(old_profiles.get(url)), new_profiles.get(url)) {
(Some(o), Some(n)) => {
let mut changes = Vec::new();
if o.url != n.url {
changes.push(change(
"profile-replaced",
&n.type_name,
&format!("Takes over from {}", o.url),
Some(o.url.clone()),
Some(n.url.clone()),
));
}
if o.base != n.base {
changes.push(change(
"base-changed",
&n.type_name,
"The profile is based on another definition",
o.base.clone(),
n.base.clone(),
));
}
let mut walker = Walker {
old,
new,
changes: &mut changes,
invariants_added: Vec::new(),
invariants_removed: Vec::new(),
};
walker.node(&n.type_name, &n.type_name, &o.tree, &n.tree);
let (added, removed) = (walker.invariants_added, walker.invariants_removed);
fold_moved_invariants(&mut changes, &added, &removed);
ProfileDiff {
url: url.clone(),
name: n.name.clone(),
title: n.title.clone(),
type_name: n.type_name.clone(),
status: if changes.is_empty() {
Status::Unchanged
} else {
Status::Changed
},
replaces: (o.url != n.url).then(|| o.url.clone()),
old_version: o.version.clone(),
new_version: n.version.clone(),
file: n.file.clone(),
changes,
}
}
(Some(o), None) => ProfileDiff {
url: url.clone(),
name: o.name.clone(),
title: o.title.clone(),
type_name: o.type_name.clone(),
status: Status::Removed,
replaces: None,
old_version: o.version.clone(),
new_version: None,
file: o.file.clone(),
changes: vec![change(
"profile-removed",
&o.type_name,
"The profile is not in the new package",
Some(url.clone()),
None,
)],
},
(None, Some(n)) => ProfileDiff {
url: url.clone(),
name: n.name.clone(),
title: n.title.clone(),
type_name: n.type_name.clone(),
status: Status::Added,
replaces: None,
old_version: None,
new_version: n.version.clone(),
file: n.file.clone(),
changes: vec![change(
"profile-added",
&n.type_name,
"The profile is new",
None,
Some(url.clone()),
)],
},
(None, None) => continue,
};
profiles.push(entry);
}
let notes = old
.skipped
.iter()
.map(|(file, reason)| format!("{old_id}: {file}: {reason}"))
.chain(
new.skipped
.iter()
.map(|(file, reason)| format!("{new_id}: {file}: {reason}")),
)
.chain(old.missing.borrow().iter().map(|base| {
format!("{old_id}: base {base} is in no loaded package; compared over the core type")
}))
.chain(new.missing.borrow().iter().map(|base| {
format!("{new_id}: base {base} is in no loaded package; compared over the core type")
}))
.chain(old.borrowed.iter().map(|base| {
format!("{old_id}: base {base} is in no listed dependency; taken from {new_id}'s")
}))
.chain(new.borrowed.iter().map(|base| {
format!("{new_id}: base {base} is in no listed dependency; taken from {old_id}'s")
}))
.collect();
Report {
old: old_id.to_owned(),
new: new_id.to_owned(),
profiles,
notes,
}
}
fn replacements(
old: &BTreeMap<String, Profile>,
new: &BTreeMap<String, Profile>,
) -> BTreeMap<String, String> {
let only_here = |side: &'_ BTreeMap<String, Profile>, other: &BTreeMap<String, Profile>| {
let mut by_type: BTreeMap<String, Vec<String>> = BTreeMap::new();
for profile in side.values() {
if !other.contains_key(&profile.url) && profile.type_name != "Extension" {
by_type
.entry(profile.type_name.clone())
.or_default()
.push(profile.url.clone());
}
}
by_type
};
let (removed, added) = (only_here(old, new), only_here(new, old));
removed
.iter()
.filter_map(
|(type_name, urls)| match (urls.as_slice(), added.get(type_name)?.as_slice()) {
([previous], [next]) => Some((next.clone(), previous.clone())),
_ => None,
},
)
.collect()
}
struct InvariantSite {
key: String,
severity: String,
index: usize,
}
fn fold_moved_invariants(
changes: &mut Vec<Change>,
added: &[InvariantSite],
removed: &[InvariantSite],
) {
let mut folded = BTreeSet::new();
for site in added {
let Some(from) = removed
.iter()
.find(|r| r.key == site.key && !folded.contains(&r.index))
else {
continue;
};
let origin = changes[from.index].clone();
let (old_expression, arrival) = (origin.old.unwrap_or_default(), &mut changes[site.index]);
let new_expression = arrival.new.clone().unwrap_or_default();
let same = from.severity == site.severity
&& same_after_move(
&origin.path,
&old_expression,
&arrival.path,
&new_expression,
);
let (rule, message) = if same {
(
"invariant-moved",
format!("Invariant {} moved from {}", site.key, origin.path),
)
} else {
(
"invariant-changed",
format!(
"Invariant {} moved from {} and changed",
site.key, origin.path
),
)
};
arrival.rule = rule;
arrival.severity = severity_of(rule);
arrival.message = message;
arrival.old = Some(old_expression);
folded.insert(from.index);
}
let mut index = 0;
changes.retain(|_| {
index += 1;
!folded.contains(&(index - 1))
});
}
fn same_after_move(from: &str, old: &str, to: &str, new: &str) -> bool {
match (to.strip_prefix(from), from.strip_prefix(to)) {
(Some(tail), _) if tail.starts_with('.') => relative_to(old, &tail[1..]) == new,
(_, Some(tail)) if tail.starts_with('.') => relative_to(new, &tail[1..]) == old,
_ => old == new,
}
}
fn relative_to(expression: &str, element: &str) -> String {
let prefix = format!("{element}.");
let mut result = String::new();
let mut rest = expression;
while let Some(at) = rest.find(&prefix) {
result.push_str(&rest[..at]);
let inside_a_path = result
.chars()
.next_back()
.is_some_and(|c| c.is_alphanumeric() || c == '.' || c == '_');
if inside_a_path {
result.push_str(&prefix);
}
rest = &rest[at + prefix.len()..];
}
result.push_str(rest);
result
}
fn change(
rule: &'static str,
path: &str,
message: &str,
old: Option<String>,
new: Option<String>,
) -> Change {
Change {
rule,
severity: severity_of(rule),
path: path.to_owned(),
message: message.to_owned(),
old,
new,
}
}
fn cardinality(max: Option<u32>) -> String {
max.map_or_else(|| "*".to_owned(), |m| m.to_string())
}
struct Walker<'a> {
old: &'a Pool,
new: &'a Pool,
changes: &'a mut Vec<Change>,
invariants_added: Vec<InvariantSite>,
invariants_removed: Vec<InvariantSite>,
}
impl Walker<'_> {
fn push(
&mut self,
rule: &'static str,
path: &str,
message: String,
old: Option<String>,
new: Option<String>,
) {
self.changes.push(Change {
rule,
severity: severity_of(rule),
path: path.to_owned(),
message,
old,
new,
});
}
fn node(&mut self, path: &str, type_name: &str, old: &Node, new: &Node) {
self.element(path, type_name, old, new, false, &BTreeMap::new());
}
fn element(
&mut self,
path: &str,
type_name: &str,
old: &Node,
new: &Node,
variant: bool,
old_siblings: &BTreeMap<String, Node>,
) {
if !variant {
self.cardinality(path, old, new);
self.types(path, old, new);
self.flags(path, old, new);
self.constraints(path, old, new);
self.slicing(path, old, new);
}
self.binding(path, old, new);
self.pattern(path, old, new);
let names: BTreeSet<&String> = old.children.keys().chain(new.children.keys()).collect();
for name in names {
let child_path = format!("{path}.{name}");
let (o, n) = (old.children.get(name), new.children.get(name));
let child_type = n
.or(o)
.and_then(|c| (c.types.len() == 1).then(|| c.types.iter().next().cloned()))
.flatten()
.unwrap_or_default();
let variant = is_variant(&new.children, name) || is_variant(&old.children, name);
match (o, n) {
(Some(o), Some(n)) => {
if variant && choice_settled(old, new, name) {
self.flags(&child_path, o, n);
}
self.element(&child_path, &child_type, o, n, variant, &old.children);
}
(Some(o), None) => {
let base = self.base(type_name, name, o);
self.element(&child_path, &child_type, o, &base, variant, &old.children);
}
(None, Some(n)) => {
let base = self.base(type_name, name, n);
self.element(&child_path, &child_type, &base, n, variant, &old.children);
}
(None, None) => {}
}
}
if variant {
return;
}
let slices: BTreeSet<&String> = old.slices.keys().chain(new.slices.keys()).collect();
for slice in slices {
let slice_path = format!("{path}:{slice}");
match (old.slices.get(slice), new.slices.get(slice)) {
(Some(o), Some(n)) => self.node(&slice_path, type_name, o, n),
(Some(_), None) => self.push(
"slice-removed",
&slice_path,
format!("Slice {slice} was removed"),
Some(slice.clone()),
None,
),
(None, Some(n)) => {
if n.min > 0 {
self.push(
"min-increased",
&slice_path,
format!("Required slice {slice} was added (min {})", n.min),
Some("0".into()),
Some(n.min.to_string()),
);
} else {
self.push(
"slice-added",
&slice_path,
format!("Optional slice {slice} was added"),
None,
Some(slice.clone()),
);
}
let supported = old_siblings.get(slice).is_some_and(|v| v.must_support);
if n.must_support && !supported {
self.push(
"must-support-added",
&slice_path,
format!("New slice {slice} is mustSupport"),
None,
Some("true".into()),
);
}
}
(None, None) => {}
}
}
}
fn base(&self, type_name: &str, name: &str, present: &Node) -> Node {
let core = self
.new
.base_node(type_name, name)
.or_else(|| self.old.base_node(type_name, name));
match core {
Some(mut base) => {
base.children.clear();
base
}
None => Node {
types: present.types.clone(),
max: present.max,
targets: present.targets.clone(),
..Node::unconstrained()
},
}
}
fn cardinality(&mut self, path: &str, old: &Node, new: &Node) {
if new.min > old.min {
self.push(
"min-increased",
path,
format!("min {} → {}", old.min, new.min),
Some(old.min.to_string()),
Some(new.min.to_string()),
);
} else if new.min < old.min {
self.push(
"min-decreased",
path,
format!("min {} → {}", old.min, new.min),
Some(old.min.to_string()),
Some(new.min.to_string()),
);
}
let narrower = match (old.max, new.max) {
(None, Some(_)) => Some(true),
(Some(_), None) => Some(false),
(Some(a), Some(b)) if b < a => Some(true),
(Some(a), Some(b)) if b > a => Some(false),
_ => None,
};
if let Some(narrower) = narrower {
self.push(
if narrower {
"max-decreased"
} else {
"max-increased"
},
path,
format!("max {} → {}", cardinality(old.max), cardinality(new.max)),
Some(cardinality(old.max)),
Some(cardinality(new.max)),
);
}
}
fn types(&mut self, path: &str, old: &Node, new: &Node) {
if !old.types.is_empty() && !new.types.is_empty() {
for removed in old.types.difference(&new.types) {
self.push(
"type-removed",
path,
format!("Type {removed} is no longer allowed"),
Some(removed.clone()),
None,
);
}
for added in new.types.difference(&old.types) {
self.push(
"type-added",
path,
format!("Type {added} is allowed"),
None,
Some(added.clone()),
);
}
}
if old.targets.is_empty() && !new.targets.is_empty() {
self.push(
"target-removed",
path,
format!("References are restricted to {}", join(&new.targets)),
None,
Some(join(&new.targets)),
);
} else if !new.targets.is_empty() {
let mut added: Vec<&String> = new.targets.difference(&old.targets).collect();
for removed in old.targets.difference(&new.targets) {
let removed_type = self.old.type_of(removed);
let narrowed = added
.iter()
.position(|a| removed_type.is_some() && self.new.type_of(a) == removed_type);
match narrowed {
Some(index) => {
let to = added.remove(index);
self.push(
"target-narrowed",
path,
format!("Target {removed} narrowed to {to}"),
Some(removed.clone()),
Some(to.clone()),
);
}
None => self.push(
"target-removed",
path,
format!("Target {removed} is no longer allowed"),
Some(removed.clone()),
None,
),
}
}
for added in added {
self.push(
"target-added",
path,
format!("Target {added} is allowed"),
None,
Some(added.clone()),
);
}
}
}
fn flags(&mut self, path: &str, old: &Node, new: &Node) {
match (old.must_support, new.must_support) {
(false, true) => self.push(
"must-support-added",
path,
"Became mustSupport".into(),
Some("false".into()),
Some("true".into()),
),
(true, false) => self.push(
"must-support-removed",
path,
"No longer mustSupport".into(),
Some("true".into()),
Some("false".into()),
),
_ => {}
}
if !path.contains('.') && old.is_modifier != new.is_modifier {
self.push(
"modifier-changed",
path,
format!("isModifier {} → {}", old.is_modifier, new.is_modifier),
Some(old.is_modifier.to_string()),
Some(new.is_modifier.to_string()),
);
}
}
fn binding(&mut self, path: &str, old: &Node, new: &Node) {
let describe = |b: &crate::model::Binding| {
format!(
"{} {}",
b.strength.as_str(),
b.value_set.as_deref().unwrap_or("(no value set)")
)
};
match (&old.binding, &new.binding) {
(None, Some(n)) if n.strength >= Strength::Extensible => {
self.push(
"binding-added",
path,
format!("Bound {}", describe(n)),
None,
Some(describe(n)),
);
}
(Some(o), Some(n)) => {
if n.strength > o.strength {
let rule = if n.strength >= Strength::Extensible {
"binding-stronger"
} else {
"binding-stronger-advisory"
};
self.push(
rule,
path,
format!("{} → {}", o.strength.as_str(), n.strength.as_str()),
Some(describe(o)),
Some(describe(n)),
);
} else if n.strength < o.strength {
self.push(
"binding-weaker",
path,
format!("{} → {}", o.strength.as_str(), n.strength.as_str()),
Some(describe(o)),
Some(describe(n)),
);
}
let plain = |vs: &Option<String>| {
vs.as_deref()
.map(|v| v.split('|').next().unwrap_or(v).to_owned())
};
if plain(&o.value_set) != plain(&n.value_set) && n.strength >= Strength::Extensible
{
self.push(
"value-set-changed",
path,
format!(
"Value set {} → {}",
o.value_set.as_deref().unwrap_or("-"),
n.value_set.as_deref().unwrap_or("-")
),
o.value_set.clone(),
n.value_set.clone(),
);
}
}
_ => {}
}
}
fn constraints(&mut self, path: &str, old: &Node, new: &Node) {
for (key, constraint) in &new.constraints {
match old.constraints.get(key) {
None => {
let rule = if constraint.severity == "error" {
"invariant-added"
} else {
"invariant-warning-added"
};
self.invariants_added.push(InvariantSite {
key: key.clone(),
severity: constraint.severity.clone(),
index: self.changes.len(),
});
self.push(
rule,
path,
format!("Invariant {key}: {}", constraint.human),
None,
Some(constraint.expression.clone()),
);
}
Some(previous) if previous.expression != constraint.expression => {
self.push(
"invariant-changed",
path,
format!("Invariant {key} changed"),
Some(previous.expression.clone()),
Some(constraint.expression.clone()),
);
}
Some(_) => {}
}
}
for (key, constraint) in &old.constraints {
if !new.constraints.contains_key(key) {
self.invariants_removed.push(InvariantSite {
key: key.clone(),
severity: constraint.severity.clone(),
index: self.changes.len(),
});
self.push(
"invariant-removed",
path,
format!("Invariant {key} was removed"),
Some(constraint.expression.clone()),
None,
);
}
}
}
fn pattern(&mut self, path: &str, old: &Node, new: &Node) {
let text = |v: &serde_json::Value| v.to_string();
match (&old.pattern, &new.pattern) {
(None, Some(n)) => self.push(
"fixed-added",
path,
format!("Fixed/pattern {}", text(n)),
None,
Some(text(n)),
),
(Some(o), None) => self.push(
"fixed-removed",
path,
"Fixed/pattern value removed".into(),
Some(text(o)),
None,
),
(Some(o), Some(n)) if o != n => self.push(
"fixed-changed",
path,
"Fixed/pattern value changed".into(),
Some(text(o)),
Some(text(n)),
),
_ => {}
}
}
fn slicing(&mut self, path: &str, old: &Node, new: &Node) {
let rules = |node: &Node| node.slicing.as_ref().map(|s| s.rules.clone());
match (rules(old).as_deref(), rules(new).as_deref()) {
(Some("closed"), Some(r)) if r != "closed" => self.push(
"slicing-opened",
path,
format!("Slicing closed → {r}"),
Some("closed".into()),
Some(r.to_owned()),
),
(o, Some("closed")) if o != Some("closed") => self.push(
"slicing-closed",
path,
format!("Slicing {} → closed", o.unwrap_or("none")),
o.map(str::to_owned),
Some("closed".into()),
),
_ => {}
}
}
}
fn choice_settled(old: &Node, new: &Node, variant: &str) -> bool {
let choice = |parent: &'_ Node| {
parent
.children
.values()
.find(|c| c.types.contains(variant))
.map(|c| (c.must_support, c.slices.is_empty()))
};
matches!(
(choice(old), choice(new)),
(Some((before, true)), Some((after, true))) if before == after
)
}
fn join(set: &BTreeSet<String>) -> String {
set.iter().cloned().collect::<Vec<_>>().join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_expression_reads_the_same_from_a_child_element() {
let old = "value.ofType(Quantity).system.empty() or value.ofType(Quantity).system = 'u'";
let new = "ofType(Quantity).system.empty() or ofType(Quantity).system = 'u'";
assert!(same_after_move(
"Observation",
old,
"Observation.value",
new
));
assert!(same_after_move(
"Observation.value",
new,
"Observation",
old
));
assert!(!same_after_move(
"Observation",
old,
"Observation.value",
old
));
}
#[test]
fn only_paths_that_start_with_the_element_are_rewritten() {
assert_eq!(
relative_to("a.value.b or value.c", "value"),
"a.value.b or c"
);
assert_eq!(relative_to("myvalue.x", "value"), "myvalue.x");
}
}