From 0715c8a98891f4204c9d29b9c591f48695c3a5df Mon Sep 17 00:00:00 2001 From: jerbly Date: Fri, 9 Jan 2026 22:26:52 -0500 Subject: [PATCH 1/5] Added OrderedF64 and switched to schemars v1 --- Cargo.lock | 21 +- Cargo.toml | 3 +- crates/weaver_common/src/lib.rs | 1 + crates/weaver_common/src/ordered_float.rs | 185 ++++++++++++++++++ crates/weaver_emit/Cargo.toml | 1 - crates/weaver_emit/src/attributes.rs | 25 +-- crates/weaver_resolved_schema/Cargo.toml | 2 +- .../allowed-external-types.toml | 2 +- crates/weaver_resolved_schema/src/value.rs | 6 +- crates/weaver_semconv/Cargo.toml | 1 - .../allowed-external-types.toml | 1 - crates/weaver_semconv/src/attribute.rs | 44 ++--- crates/weaver_semconv/src/lib.rs | 32 ++- crates/weaver_semconv/src/semconv.rs | 48 +---- .../weaver_semconv/src/v2/attribute_group.rs | 50 +---- src/registry/json_schema.rs | 8 +- ui/src/routes/Schema.svelte | 24 +-- 17 files changed, 264 insertions(+), 190 deletions(-) create mode 100644 crates/weaver_common/src/ordered_float.rs diff --git a/Cargo.lock b/Cargo.lock index e19888971..e8af00b47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3216,9 +3216,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" dependencies = [ "num-traits", - "rand 0.8.5", - "schemars", - "serde", ] [[package]] @@ -3602,7 +3599,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -3629,7 +3626,6 @@ dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", - "serde", ] [[package]] @@ -3669,7 +3665,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.16", - "serde", ] [[package]] @@ -4062,21 +4057,23 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.22" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" dependencies = [ "dyn-clone", + "ref-cast", "schemars_derive", + "semver", "serde", "serde_json", ] [[package]] name = "schemars_derive" -version = "0.8.22" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +checksum = "4908ad288c5035a8eb12cfdf0d49270def0a268ee162b75eeee0f85d155a7c45" dependencies = [ "proc-macro2", "quote", @@ -5390,7 +5387,6 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry-stdout", "opentelemetry_sdk", - "ordered-float", "serde", "thiserror 2.0.17", "tokio", @@ -5479,11 +5475,11 @@ name = "weaver_resolved_schema" version = "0.20.0" dependencies = [ "log", - "ordered-float", "schemars", "serde", "serde_json", "thiserror 2.0.17", + "weaver_common", "weaver_semconv", "weaver_version", ] @@ -5529,7 +5525,6 @@ dependencies = [ "jsonschema 0.33.0", "log", "miette", - "ordered-float", "regex", "saphyr", "schemars", diff --git a/Cargo.toml b/Cargo.toml index f9152aa2f..b8ee53630 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,6 @@ url = "2.5.4" ureq = "3.1.4" regex = "1.11.3" rayon = "1.11.0" -ordered-float = { version = "5.1.0", features = ["serde", "schemars"] } walkdir = "2.5.0" anyhow = "1.0.100" itertools = "0.14.0" @@ -51,7 +50,7 @@ globset = { version = "0.4.16", features = ["serde1"] } miette = { version = "7.6.0", features = ["fancy", "serde"] } include_dir = "0.7.4" tempfile = "3.23.0" -schemars = "0.8.22" +schemars = { version = "1", features = ["semver1"] } dirs = "6.0.0" once_cell = "1.21.3" opentelemetry = { version = "0.31.0", features = ["trace", "metrics", "logs"] } diff --git a/crates/weaver_common/src/lib.rs b/crates/weaver_common/src/lib.rs index 297b31958..0d503b7cf 100644 --- a/crates/weaver_common/src/lib.rs +++ b/crates/weaver_common/src/lib.rs @@ -4,6 +4,7 @@ pub mod diagnostic; pub mod error; +pub mod ordered_float; pub mod result; #[cfg(test)] pub mod test; diff --git a/crates/weaver_common/src/ordered_float.rs b/crates/weaver_common/src/ordered_float.rs new file mode 100644 index 000000000..9d57109b2 --- /dev/null +++ b/crates/weaver_common/src/ordered_float.rs @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A custom OrderedF64 implementation that supports schemars v1. + +use schemars::{json_schema, JsonSchema, Schema, SchemaGenerator}; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use std::cmp::Ordering; +use std::fmt::{self, Debug, Display, Formatter}; +use std::hash::{Hash, Hasher}; +use std::ops::{Deref, DerefMut}; + +/// A wrapper around f64 that provides total ordering and hashing. +/// +/// Treats NaN values as equal to each other and greater than all other values. +#[derive(Clone, Copy, Default, Serialize, Deserialize)] +#[serde(transparent)] +#[repr(transparent)] +pub struct OrderedF64(pub f64); + +impl Deref for OrderedF64 { + type Target = f64; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for OrderedF64 { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Debug for OrderedF64 { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Debug::fmt(&self.0, f) + } +} + +impl Display for OrderedF64 { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl PartialEq for OrderedF64 { + #[inline] + fn eq(&self, other: &Self) -> bool { + if self.0.is_nan() && other.0.is_nan() { + true + } else { + self.0 == other.0 + } + } +} + +impl Eq for OrderedF64 {} + +impl PartialOrd for OrderedF64 { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for OrderedF64 { + fn cmp(&self, other: &Self) -> Ordering { + match self.0.partial_cmp(&other.0) { + Some(ordering) => ordering, + None => { + if self.0.is_nan() { + if other.0.is_nan() { + Ordering::Equal + } else { + Ordering::Greater + } + } else { + Ordering::Less + } + } + } + } +} + +impl Hash for OrderedF64 { + fn hash(&self, state: &mut H) { + let bits = if self.0.is_nan() { + f64::NAN.to_bits() + } else if self.0 == 0.0 { + 0u64 + } else { + self.0.to_bits() + }; + bits.hash(state); + } +} + +impl From for OrderedF64 { + #[inline] + fn from(val: f64) -> Self { + OrderedF64(val) + } +} + +impl From for f64 { + #[inline] + fn from(val: OrderedF64) -> Self { + val.0 + } +} + +impl JsonSchema for OrderedF64 { + fn schema_name() -> Cow<'static, str> { + "double".into() + } + + fn schema_id() -> Cow<'static, str> { + concat!(module_path!(), "::OrderedF64").into() + } + + fn json_schema(_gen: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "number", + "format": "double" + }) + } + + fn inline_schema() -> bool { + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::hash_map::DefaultHasher; + + #[test] + fn test_ordering() { + assert!(OrderedF64(1.0) < OrderedF64(2.0)); + assert!(OrderedF64(2.0) > OrderedF64(1.0)); + assert!(OrderedF64(1.0) == OrderedF64(1.0)); + } + + #[test] + fn test_nan_ordering() { + let nan = OrderedF64(f64::NAN); + let one = OrderedF64(1.0); + assert!(nan > one); + assert!(nan == nan); + } + + #[test] + fn test_hash_consistency() { + fn hash(value: &T) -> u64 { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() + } + + assert_eq!(hash(&OrderedF64(1.0)), hash(&OrderedF64(1.0))); + assert_eq!(hash(&OrderedF64(0.0)), hash(&OrderedF64(-0.0))); + assert_eq!(hash(&OrderedF64(f64::NAN)), hash(&OrderedF64(f64::NAN))); + } + + #[test] + fn test_serialization() { + let value = OrderedF64(3.15); + let serialized = serde_json::to_string(&value).unwrap(); + assert_eq!(serialized, "3.15"); + + let deserialized: OrderedF64 = serde_json::from_str("3.15").unwrap(); + assert_eq!(deserialized, value); + } + + #[test] + fn test_json_schema() { + let schema = schemars::schema_for!(OrderedF64); + let schema_str = serde_json::to_string_pretty(&schema).unwrap(); + assert!(schema_str.contains("\"type\": \"number\"")); + } +} diff --git a/crates/weaver_emit/Cargo.toml b/crates/weaver_emit/Cargo.toml index 13754be46..abf05d92b 100644 --- a/crates/weaver_emit/Cargo.toml +++ b/crates/weaver_emit/Cargo.toml @@ -18,7 +18,6 @@ opentelemetry_sdk.workspace = true opentelemetry-otlp.workspace = true opentelemetry-stdout.workspace = true tokio.workspace = true -ordered-float.workspace = true thiserror.workspace = true serde.workspace = true miette.workspace = true diff --git a/crates/weaver_emit/src/attributes.rs b/crates/weaver_emit/src/attributes.rs index dda97daa3..e6700a7b8 100644 --- a/crates/weaver_emit/src/attributes.rs +++ b/crates/weaver_emit/src/attributes.rs @@ -3,6 +3,7 @@ //! Translations from Weaver to Otel for attributes. use opentelemetry::{Array, KeyValue, Value}; +use weaver_common::ordered_float::OrderedF64; use weaver_resolved_schema::attribute::Attribute; use weaver_semconv::attribute::ValueSpec; use weaver_semconv::attribute::{ @@ -48,7 +49,7 @@ fn internal_get_attribute_name_value( PrimitiveOrArrayTypeSpec::Double => match &examples { Some(Examples::Double(d)) => Value::F64(f64::from(*d)), Some(Examples::Doubles(doubles)) => { - Value::F64(f64::from(*doubles.first().unwrap_or((&3.13).into()))) + Value::F64(f64::from(*doubles.first().unwrap_or(&OrderedF64(3.13)))) } _ => Value::F64(3.13), }, @@ -81,7 +82,7 @@ fn internal_get_attribute_name_value( // Double-based examples Some(Examples::Double(d)) => Value::F64(f64::from(*d)), Some(Examples::Doubles(doubles)) => { - Value::F64(f64::from(*doubles.first().unwrap_or((&3.13).into()))) + Value::F64(f64::from(*doubles.first().unwrap_or(&OrderedF64(3.13)))) } Some(Examples::ListOfDoubles(list_of_doubles)) => Value::Array(Array::F64( list_of_doubles @@ -199,7 +200,7 @@ fn internal_get_attribute_name_value( mod tests { use super::*; use opentelemetry::{Array, KeyValue, Value}; - use ordered_float::OrderedFloat; + use weaver_common::ordered_float::OrderedF64; use weaver_resolved_schema::attribute::Attribute; use weaver_semconv::attribute::{ AttributeType, EnumEntriesSpec, Examples, PrimitiveOrArrayTypeSpec, RequirementLevel, @@ -282,7 +283,7 @@ mod tests { let attr = create_test_attribute( "test.double", AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::Double), - Some(Examples::Double(OrderedFloat(3.15))), + Some(Examples::Double(OrderedF64(3.15))), ); let kv = get_attribute_name_value(&attr); assert_eq!(kv, KeyValue::new("test.double", 3.15)); @@ -294,8 +295,8 @@ mod tests { "test.double", AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::Double), Some(Examples::Doubles(vec![ - OrderedFloat(3.15), - OrderedFloat(2.71), + OrderedF64(3.15), + OrderedF64(2.71), ])), ); let kv = get_attribute_name_value(&attr); @@ -410,8 +411,8 @@ mod tests { "test.doubles", AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::Doubles), Some(Examples::Doubles(vec![ - OrderedFloat(1.1), - OrderedFloat(2.2), + OrderedF64(1.1), + OrderedF64(2.2), ])), ); let kv = get_attribute_name_value(&attr); @@ -424,8 +425,8 @@ mod tests { "test.doubles", AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::Doubles), Some(Examples::ListOfDoubles(vec![ - vec![OrderedFloat(1.1), OrderedFloat(2.2)], - vec![OrderedFloat(3.3), OrderedFloat(4.4)], + vec![OrderedF64(1.1), OrderedF64(2.2)], + vec![OrderedF64(3.3), OrderedF64(4.4)], ])), ); let kv = get_attribute_name_value(&attr); @@ -560,7 +561,7 @@ mod tests { members: vec![ EnumEntriesSpec { id: "first".to_owned(), - value: ValueSpec::Double(OrderedFloat(1.5)), + value: ValueSpec::Double(OrderedF64(1.5)), brief: None, note: None, stability: None, @@ -569,7 +570,7 @@ mod tests { }, EnumEntriesSpec { id: "second".to_owned(), - value: ValueSpec::Double(OrderedFloat(2.5)), + value: ValueSpec::Double(OrderedF64(2.5)), brief: None, note: None, stability: None, diff --git a/crates/weaver_resolved_schema/Cargo.toml b/crates/weaver_resolved_schema/Cargo.toml index edb842f42..62c321163 100644 --- a/crates/weaver_resolved_schema/Cargo.toml +++ b/crates/weaver_resolved_schema/Cargo.toml @@ -12,12 +12,12 @@ rust-version.workspace = true workspace = true [dependencies] +weaver_common = { path = "../weaver_common" } weaver_version = { path = "../weaver_version" } weaver_semconv = { path = "../weaver_semconv" } thiserror.workspace = true serde.workspace = true -ordered-float.workspace = true schemars.workspace = true log.workspace = true diff --git a/crates/weaver_resolved_schema/allowed-external-types.toml b/crates/weaver_resolved_schema/allowed-external-types.toml index ee279afb3..9b2e9a260 100644 --- a/crates/weaver_resolved_schema/allowed-external-types.toml +++ b/crates/weaver_resolved_schema/allowed-external-types.toml @@ -5,7 +5,7 @@ allowed_external_types = [ "serde::ser::Serialize", "serde::de::Deserialize", - "ordered_float::OrderedFloat", # ToDo: Remove this dependency before version 1.0 + "weaver_common::*", "weaver_semconv::*", "weaver_version::*", "schemars::JsonSchema", diff --git a/crates/weaver_resolved_schema/src/value.rs b/crates/weaver_resolved_schema/src/value.rs index 41a8c3701..79cd812c7 100644 --- a/crates/weaver_resolved_schema/src/value.rs +++ b/crates/weaver_resolved_schema/src/value.rs @@ -2,9 +2,9 @@ //! Specification of a resolved value. -use ordered_float::OrderedFloat; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use weaver_common::ordered_float::OrderedF64; /// The different types of values. #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, JsonSchema)] @@ -19,7 +19,7 @@ pub enum Value { /// A double value. Double { /// The value - value: OrderedFloat, + value: OrderedF64, }, /// A string value. String { @@ -32,7 +32,7 @@ impl Value { /// Creates a double value from a f64. pub fn from_f64(value: f64) -> Self { Value::Double { - value: OrderedFloat(value), + value: OrderedF64(value), } } } diff --git a/crates/weaver_semconv/Cargo.toml b/crates/weaver_semconv/Cargo.toml index 1a1e21ea5..aff22aad4 100644 --- a/crates/weaver_semconv/Cargo.toml +++ b/crates/weaver_semconv/Cargo.toml @@ -23,7 +23,6 @@ serde_yaml.workspace = true serde_json.workspace = true thiserror.workspace = true ureq.workspace = true -ordered-float.workspace = true miette.workspace = true schemars.workspace = true regex.workspace = true diff --git a/crates/weaver_semconv/allowed-external-types.toml b/crates/weaver_semconv/allowed-external-types.toml index d8f2acdc2..a682ceb0a 100644 --- a/crates/weaver_semconv/allowed-external-types.toml +++ b/crates/weaver_semconv/allowed-external-types.toml @@ -8,7 +8,6 @@ allowed_external_types = [ "serde::de::Deserializer", "weaver_common::*", "weaver_cache::RegistryRepo", - "ordered_float::OrderedFloat", # ToDo: Remove this dependency before version 1.0 "miette::protocol::Diagnostic", "schemars::JsonSchema", "serde_yaml::value::Value", diff --git a/crates/weaver_semconv/src/attribute.rs b/crates/weaver_semconv/src/attribute.rs index 2c75a646f..b965854c9 100644 --- a/crates/weaver_semconv/src/attribute.rs +++ b/crates/weaver_semconv/src/attribute.rs @@ -8,7 +8,7 @@ use crate::any_value::AnyValueSpec; use crate::deprecated::Deprecated; use crate::stability::Stability; use crate::{Error, YamlValue}; -use ordered_float::OrderedFloat; +use weaver_common::ordered_float::OrderedF64; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -419,7 +419,7 @@ pub enum ValueSpec { Int(i64), /// A double value. #[cfg_attr(feature = "openapi", schema(value_type = f64))] - Double(OrderedFloat), + Double(OrderedF64), /// A string value. String(String), /// A boolean value. @@ -451,7 +451,7 @@ impl From for ValueSpec { impl From for ValueSpec { /// Converts a f64 into a ValueSpec. fn from(value: f64) -> Self { - ValueSpec::Double(OrderedFloat(value)) + ValueSpec::Double(OrderedF64(value)) } } @@ -483,7 +483,7 @@ pub enum Examples { Int(i64), /// A double example. #[cfg_attr(feature = "openapi", schema(value_type = f64))] - Double(OrderedFloat), + Double(OrderedF64), /// A string example. String(String), /// A any example. @@ -492,7 +492,7 @@ pub enum Examples { Ints(Vec), /// A array of doubles example. #[cfg_attr(feature = "openapi", schema(value_type = Vec))] - Doubles(Vec>), + Doubles(Vec), /// A array of bools example. Bools(Vec), /// A array of strings example. @@ -503,7 +503,7 @@ pub enum Examples { ListOfInts(Vec>), /// List of arrays of doubles example. #[cfg_attr(feature = "openapi", schema(value_type = Vec>))] - ListOfDoubles(Vec>>), + ListOfDoubles(Vec>), /// List of arrays of bools example. ListOfBools(Vec>), /// List of arrays of strings example. @@ -738,13 +738,13 @@ impl Examples { /// Creates an example from a f64. #[must_use] pub fn from_f64(value: f64) -> Self { - Examples::Double(OrderedFloat(value)) + Examples::Double(OrderedF64(value)) } /// Creates an example from several f64. #[must_use] pub fn from_f64s(values: Vec) -> Self { - Examples::Doubles(values.into_iter().map(OrderedFloat).collect()) + Examples::Doubles(values.into_iter().map(OrderedF64).collect()) } } @@ -755,7 +755,7 @@ mod tests { #[test] fn test_value_spec_display() { assert_eq!(format!("{}", ValueSpec::Int(42)), "42"); - assert_eq!(format!("{}", ValueSpec::Double(OrderedFloat(42.0))), "42"); + assert_eq!(format!("{}", ValueSpec::Double(OrderedF64(42.0))), "42"); assert_eq!(format!("{}", ValueSpec::String("42".to_owned())), "42"); } @@ -968,7 +968,7 @@ mod tests { fn test_examples_from_f64() { assert_eq!( Examples::from_f64(42.0), - Examples::Double(OrderedFloat(42.0)) + Examples::Double(OrderedF64(42.0)) ); } @@ -976,7 +976,7 @@ mod tests { fn test_examples_from_f64s() { assert_eq!( Examples::from_f64s(vec![42.0, 43.0]), - Examples::Doubles(vec![OrderedFloat(42.0), OrderedFloat(43.0)]) + Examples::Doubles(vec![OrderedF64(42.0), OrderedF64(43.0)]) ); } @@ -1045,7 +1045,7 @@ mod tests { fn test_examples_double() { let yaml = "---\n3.15"; let ex: Examples = serde_yaml::from_str(yaml).unwrap(); - assert_eq!(ex, Examples::Double(OrderedFloat(3.15))); + assert_eq!(ex, Examples::Double(OrderedF64(3.15))); } #[test] @@ -1078,7 +1078,7 @@ mod tests { let ex: Examples = serde_yaml::from_str(yaml).unwrap(); assert_eq!( ex, - Examples::Doubles(vec![OrderedFloat(3.15), OrderedFloat(2.71)]) + Examples::Doubles(vec![OrderedF64(3.15), OrderedF64(2.71)]) ); } @@ -1097,7 +1097,7 @@ mod tests { ex, Examples::Anys(vec![ ValueSpec::Int(1), - ValueSpec::Double(OrderedFloat(2.0)), + ValueSpec::Double(OrderedF64(2.0)), ValueSpec::String("text".to_owned()), ValueSpec::Bool(true), ]) @@ -1118,8 +1118,8 @@ mod tests { assert_eq!( ex, Examples::ListOfDoubles(vec![ - vec![OrderedFloat(3.15), OrderedFloat(2.71)], - vec![OrderedFloat(1.41), OrderedFloat(1.61)] + vec![OrderedF64(3.15), OrderedF64(2.71)], + vec![OrderedF64(1.41), OrderedF64(1.61)] ]) ); } @@ -1161,8 +1161,8 @@ mod tests { assert_eq!( ex, Examples::ListOfDoubles(vec![ - vec![OrderedFloat(3.15), OrderedFloat(2.71)], - vec![OrderedFloat(1.41), OrderedFloat(1.61)] + vec![OrderedF64(3.15), OrderedF64(2.71)], + vec![OrderedF64(1.41), OrderedF64(1.61)] ]) ); } @@ -1326,7 +1326,7 @@ mod tests { .is_err()); // === Test double-like examples === - let examples = Examples::Double(OrderedFloat(42.0)); + let examples = Examples::Double(OrderedF64(42.0)); assert!(examples .validate(&attr_double, "grp", "attr", "url") .into_result_failing_non_fatal() @@ -1336,7 +1336,7 @@ mod tests { .into_result_failing_non_fatal() .is_err()); - let examples = Examples::Doubles(vec![OrderedFloat(42.0), OrderedFloat(43.0)]); + let examples = Examples::Doubles(vec![OrderedF64(42.0), OrderedF64(43.0)]); assert!(examples .validate(&attr_double, "grp", "attr", "url") .into_result_failing_non_fatal() @@ -1347,8 +1347,8 @@ mod tests { .is_err()); let examples = Examples::ListOfDoubles(vec![ - vec![OrderedFloat(42.0), OrderedFloat(43.0)], - vec![OrderedFloat(44.0), OrderedFloat(45.0)], + vec![OrderedF64(42.0), OrderedF64(43.0)], + vec![OrderedF64(44.0), OrderedF64(45.0)], ]); assert!(examples .validate(&attr_doubles, "grp", "attr", "url") diff --git a/crates/weaver_semconv/src/lib.rs b/crates/weaver_semconv/src/lib.rs index e7064f1da..06c561815 100644 --- a/crates/weaver_semconv/src/lib.rs +++ b/crates/weaver_semconv/src/lib.rs @@ -4,8 +4,7 @@ use crate::Error::CompoundError; use miette::{Diagnostic, NamedSource, SourceSpan}; -use schemars::schema::{InstanceType, Schema}; -use schemars::{JsonSchema, SchemaGenerator}; +use schemars::{json_schema, JsonSchema, Schema, SchemaGenerator}; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::hash::Hasher; @@ -392,28 +391,19 @@ impl From for DiagnosticMessages { pub struct YamlValue(pub serde_yaml::value::Value); impl JsonSchema for YamlValue { - fn schema_name() -> String { - "YamlValue".to_owned() + fn schema_name() -> Cow<'static, str> { + "YamlValue".into() + } + + fn schema_id() -> Cow<'static, str> { + concat!(module_path!(), "::YamlValue").into() } fn json_schema(_: &mut SchemaGenerator) -> Schema { - // Create a schema that accepts any type - let schema = schemars::schema::SchemaObject { - instance_type: Some( - vec![ - InstanceType::Null, - InstanceType::Boolean, - InstanceType::Object, - InstanceType::Array, - InstanceType::Number, - InstanceType::String, - ] - .into(), - ), - ..Default::default() - }; - - Schema::Object(schema) + // Accept any JSON type + json_schema!({ + "type": ["null", "boolean", "object", "array", "number", "string"] + }) } } diff --git a/crates/weaver_semconv/src/semconv.rs b/crates/weaver_semconv/src/semconv.rs index e21fb6ac6..017ab3250 100644 --- a/crates/weaver_semconv/src/semconv.rs +++ b/crates/weaver_semconv/src/semconv.rs @@ -25,7 +25,7 @@ pub enum SemConvSpec { } /// A versioned semantic convention file. -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[serde(tag = "version")] pub enum Versioned { /// Version 1 of the semantic convention schema. @@ -36,52 +36,6 @@ pub enum Versioned { V2(SemConvSpecV2), } -// Note: We automatically create the Schemars code and provide `allow(unused_qualifications)` to work around schemars limitations. -// You can use `cargo expand -p weaver_semconv` to find this code and generate it in the future. -const _: () = { - #[automatically_derived] - #[allow(unused_braces, unused_qualifications)] - impl schemars::JsonSchema for Versioned { - fn schema_name() -> std::string::String { - "Versioned".to_owned() - } - fn schema_id() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed("weaver_semconv::semconv::Versioned") - } - fn json_schema(generator: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { - schemars::_private::metadata::add_description( - schemars::schema::Schema::Object(schemars::schema::SchemaObject { - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - one_of: Some(<[_]>::into_vec(Box::new([ - schemars::_private::metadata::add_description( - schemars::_private::new_internally_tagged_enum( - "version", "1", false, - ), - "Version 1 of the semantic convention schema.", - ) - .flatten( - ::json_schema(generator), - ), - schemars::_private::metadata::add_description( - schemars::_private::new_internally_tagged_enum( - "version", "2", false, - ), - "Version 2 of the semantic convention schema.", - ) - .flatten( - ::json_schema(generator), - ), - ]))), - ..Default::default() - })), - ..Default::default() - }), - "A versioned semantic convention file.", - ) - } - } -}; - /// A semantic convention file as defined [here](/schemas/semconv.schema.json) /// A semconv file is a collection of semantic convention groups (i.e. [`GroupSpec`]). #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, PartialEq)] diff --git a/crates/weaver_semconv/src/v2/attribute_group.rs b/crates/weaver_semconv/src/v2/attribute_group.rs index 5d511090c..77fbb5481 100644 --- a/crates/weaver_semconv/src/v2/attribute_group.rs +++ b/crates/weaver_semconv/src/v2/attribute_group.rs @@ -45,7 +45,7 @@ pub struct PublicAttributeGroup { } /// Attribute group definition. -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[serde(tag = "visibility")] #[serde(rename_all = "snake_case")] #[serde(deny_unknown_fields)] @@ -56,54 +56,6 @@ pub enum AttributeGroup { Public(PublicAttributeGroup), } -// Note: We automatically create the Schemars code and provide `allow(unused_qualifications)` to work around schemars limitations. -// You can use `cargo expand -p weaver_semconv` to find this code and generate it in the future. -const _: () = { - #[automatically_derived] - #[allow(unused_braces)] - impl JsonSchema for AttributeGroup { - fn schema_name() -> String { - "AttributeGroup".to_owned() - } - fn schema_id() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed("weaver_semconv::v2::attribute_group::AttributeGroup") - } - fn json_schema(generator: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { - schemars::_private::metadata::add_description( - schemars::schema::Schema::Object(schemars::schema::SchemaObject { - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - one_of: Some(<[_]>::into_vec(Box::new([ - schemars::_private::metadata::add_description( - schemars::_private::new_internally_tagged_enum( - "visibility", - "internal", - true, - ), - "An internal attribute group", - ) - .flatten( - ::json_schema(generator), - ), - schemars::_private::metadata::add_description( - schemars::_private::new_internally_tagged_enum( - "visibility", - "public", - true, - ), - "A public attribute group", - ) - .flatten(::json_schema(generator)), - ]))), - ..Default::default() - })), - ..Default::default() - }), - "Attribute group definition.", - ) - } - } -}; - impl AttributeGroup { /// Converts a v2 attribute group into a v1 GroupSpec. #[must_use] diff --git a/src/registry/json_schema.rs b/src/registry/json_schema.rs index 64f7ee44c..2bbfdbf78 100644 --- a/src/registry/json_schema.rs +++ b/src/registry/json_schema.rs @@ -164,14 +164,14 @@ mod tests { let value = serde_json::from_str::(&json_content) .expect("Failed to parse JSON"); - let definitions = value + let defs = value .as_object() .expect("Expected a JSON object") - .get("definitions"); + .get("$defs"); assert!( - definitions.is_some(), - "Expected a 'definitions' key in the JSON schema" + defs.is_some(), + "Expected a '$defs' key in the JSON schema" ); } } diff --git a/ui/src/routes/Schema.svelte b/ui/src/routes/Schema.svelte index dced2d304..26dda50ab 100644 --- a/ui/src/routes/Schema.svelte +++ b/ui/src/routes/Schema.svelte @@ -79,7 +79,7 @@ }); // Extract definitions from schema - const definitions = $derived(schema?.definitions ? Object.keys(schema.definitions).sort() : []); + const definitions = $derived(schema?.['$defs'] ? Object.keys(schema['$defs']).sort() : []); function selectDefinition(name) { selectedDefinition = name; @@ -108,13 +108,13 @@ // Handle nested arrays (array of array) if (prop.items.type === 'array' && prop.items.items) { const innerType = prop.items.items.$ref - ? prop.items.items.$ref.replace('#/definitions/', '') + ? prop.items.items.$ref.replace('#/$defs/', '') : prop.items.items.type || 'any'; return `array of array of ${innerType}`; } // Handle simple arrays const itemType = prop.items.$ref - ? prop.items.$ref.replace('#/definitions/', '') + ? prop.items.$ref.replace('#/$defs/', '') : prop.items.type || 'any'; // If it's a union type like ["array", "null"], add the null if not skipping @@ -133,7 +133,7 @@ (Array.isArray(prop.type) && prop.type.includes('object')); if (hasObjectType && prop.additionalProperties) { const valueType = prop.additionalProperties.$ref - ? prop.additionalProperties.$ref.replace('#/definitions/', '') + ? prop.additionalProperties.$ref.replace('#/$defs/', '') : prop.additionalProperties.type || 'any'; // If it's a union type like ["object", "null"], filter out null if skipNull is true @@ -151,21 +151,21 @@ return types.join(' | '); } if (prop.type) return prop.type; - if (prop.$ref) return prop.$ref.replace('#/definitions/', ''); + if (prop.$ref) return prop.$ref.replace('#/$defs/', ''); if (prop.allOf) { // allOf is typically used for single type references if (prop.allOf.length === 1 && prop.allOf[0].$ref) { - return prop.allOf[0].$ref.replace('#/definitions/', ''); + return prop.allOf[0].$ref.replace('#/$defs/', ''); } - return prop.allOf.map(t => t.$ref ? t.$ref.replace('#/definitions/', '') : t.type || 'object').join(' & '); + return prop.allOf.map(t => t.$ref ? t.$ref.replace('#/$defs/', '') : t.type || 'object').join(' & '); } if (prop.anyOf) { - const types = prop.anyOf.map(t => t.$ref ? t.$ref.replace('#/definitions/', '') : t.type || 'null'); + const types = prop.anyOf.map(t => t.$ref ? t.$ref.replace('#/$defs/', '') : t.type || 'null'); const filtered = skipNull ? types.filter(t => t !== 'null') : types; return filtered.join(' | '); } if (prop.oneOf) { - const types = prop.oneOf.map(t => t.$ref ? t.$ref.replace('#/definitions/', '') : t.type || 'null'); + const types = prop.oneOf.map(t => t.$ref ? t.$ref.replace('#/$defs/', '') : t.type || 'null'); const filtered = skipNull ? types.filter(t => t !== 'null') : types; return filtered.join(' | '); } @@ -185,7 +185,7 @@ // Check if it contains " | " (union type) if (typeStr.includes(' | ')) return false; // Otherwise, it's likely a definition reference - return schema?.definitions && schema.definitions[typeStr] !== undefined; + return schema?.['$defs'] && schema['$defs'][typeStr] !== undefined; } // Parse a type string and return clickable parts @@ -533,8 +533,8 @@ - {:else if selectedDefinition && schema?.definitions[selectedDefinition]} - {@const def = schema.definitions[selectedDefinition]} + {:else if selectedDefinition && schema?.['$defs'][selectedDefinition]} + {@const def = schema['$defs'][selectedDefinition]}
From 43e699fcd6d56e5c9152d8105a8722813985b6cf Mon Sep 17 00:00:00 2001 From: jerbly Date: Fri, 9 Jan 2026 22:43:05 -0500 Subject: [PATCH 2/5] fmt --- crates/weaver_emit/src/attributes.rs | 10 ++-------- crates/weaver_semconv/src/attribute.rs | 7 ++----- src/registry/json_schema.rs | 5 +---- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/crates/weaver_emit/src/attributes.rs b/crates/weaver_emit/src/attributes.rs index e6700a7b8..50d3933eb 100644 --- a/crates/weaver_emit/src/attributes.rs +++ b/crates/weaver_emit/src/attributes.rs @@ -294,10 +294,7 @@ mod tests { let attr = create_test_attribute( "test.double", AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::Double), - Some(Examples::Doubles(vec![ - OrderedF64(3.15), - OrderedF64(2.71), - ])), + Some(Examples::Doubles(vec![OrderedF64(3.15), OrderedF64(2.71)])), ); let kv = get_attribute_name_value(&attr); assert_eq!(kv, KeyValue::new("test.double", 3.15)); @@ -410,10 +407,7 @@ mod tests { let attr = create_test_attribute( "test.doubles", AttributeType::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::Doubles), - Some(Examples::Doubles(vec![ - OrderedF64(1.1), - OrderedF64(2.2), - ])), + Some(Examples::Doubles(vec![OrderedF64(1.1), OrderedF64(2.2)])), ); let kv = get_attribute_name_value(&attr); assert_eq!(kv.value, Value::Array(Array::F64(vec![1.1, 2.2]))); diff --git a/crates/weaver_semconv/src/attribute.rs b/crates/weaver_semconv/src/attribute.rs index b965854c9..102eed8f6 100644 --- a/crates/weaver_semconv/src/attribute.rs +++ b/crates/weaver_semconv/src/attribute.rs @@ -8,12 +8,12 @@ use crate::any_value::AnyValueSpec; use crate::deprecated::Deprecated; use crate::stability::Stability; use crate::{Error, YamlValue}; -use weaver_common::ordered_float::OrderedF64; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::fmt::{Display, Formatter}; use std::ops::Not; +use weaver_common::ordered_float::OrderedF64; use weaver_common::result::WResult; use AttributeType::{Enum, PrimitiveOrArray, Template}; @@ -966,10 +966,7 @@ mod tests { #[test] fn test_examples_from_f64() { - assert_eq!( - Examples::from_f64(42.0), - Examples::Double(OrderedF64(42.0)) - ); + assert_eq!(Examples::from_f64(42.0), Examples::Double(OrderedF64(42.0))); } #[test] diff --git a/src/registry/json_schema.rs b/src/registry/json_schema.rs index 2bbfdbf78..0060c43ca 100644 --- a/src/registry/json_schema.rs +++ b/src/registry/json_schema.rs @@ -169,10 +169,7 @@ mod tests { .expect("Expected a JSON object") .get("$defs"); - assert!( - defs.is_some(), - "Expected a '$defs' key in the JSON schema" - ); + assert!(defs.is_some(), "Expected a '$defs' key in the JSON schema"); } } } From 0d68dfd491e636b05de2fcf7e6b241c4afc7b7cf Mon Sep 17 00:00:00 2001 From: jerbly Date: Sat, 10 Jan 2026 13:01:50 -0500 Subject: [PATCH 3/5] improved test coverage --- crates/weaver_common/src/ordered_float.rs | 82 ++++++++++++++++++----- 1 file changed, 66 insertions(+), 16 deletions(-) diff --git a/crates/weaver_common/src/ordered_float.rs b/crates/weaver_common/src/ordered_float.rs index 9d57109b2..54e58dd6e 100644 --- a/crates/weaver_common/src/ordered_float.rs +++ b/crates/weaver_common/src/ordered_float.rs @@ -139,41 +139,69 @@ mod tests { use std::collections::hash_map::DefaultHasher; #[test] - fn test_ordering() { - assert!(OrderedF64(1.0) < OrderedF64(2.0)); - assert!(OrderedF64(2.0) > OrderedF64(1.0)); + fn test_deref() { + assert_eq!(*OrderedF64(2.5), 2.5); + } + + #[test] + fn test_deref_mut() { + let mut value = OrderedF64(2.5); + *value = 3.5; + assert_eq!(*value, 3.5); + } + + #[test] + fn test_debug() { + assert_eq!(format!("{:?}", OrderedF64(2.5)), "2.5"); + } + + #[test] + fn test_display() { + assert_eq!(format!("{}", OrderedF64(2.5)), "2.5"); + } + + #[test] + fn test_partial_eq() { assert!(OrderedF64(1.0) == OrderedF64(1.0)); + assert!(OrderedF64(1.0) != OrderedF64(2.0)); + assert!(OrderedF64(f64::NAN) == OrderedF64(f64::NAN)); + } + + #[test] + fn test_partial_ord() { + assert_eq!(OrderedF64(1.0).partial_cmp(&OrderedF64(2.0)), Some(Ordering::Less)); } #[test] - fn test_nan_ordering() { - let nan = OrderedF64(f64::NAN); - let one = OrderedF64(1.0); - assert!(nan > one); - assert!(nan == nan); + fn test_ord() { + assert_eq!(OrderedF64(1.0).cmp(&OrderedF64(2.0)), Ordering::Less); + assert_eq!(OrderedF64(f64::NAN).cmp(&OrderedF64(1.0)), Ordering::Greater); + assert_eq!(OrderedF64(1.0).cmp(&OrderedF64(f64::NAN)), Ordering::Less); + assert_eq!(OrderedF64(f64::NAN).cmp(&OrderedF64(f64::NAN)), Ordering::Equal); } #[test] - fn test_hash_consistency() { + fn test_hash() { fn hash(value: &T) -> u64 { let mut hasher = DefaultHasher::new(); value.hash(&mut hasher); hasher.finish() } - assert_eq!(hash(&OrderedF64(1.0)), hash(&OrderedF64(1.0))); assert_eq!(hash(&OrderedF64(0.0)), hash(&OrderedF64(-0.0))); assert_eq!(hash(&OrderedF64(f64::NAN)), hash(&OrderedF64(f64::NAN))); } #[test] - fn test_serialization() { - let value = OrderedF64(3.15); - let serialized = serde_json::to_string(&value).unwrap(); - assert_eq!(serialized, "3.15"); + fn test_from_f64() { + let value: OrderedF64 = 2.5.into(); + assert_eq!(*value, 2.5); + } - let deserialized: OrderedF64 = serde_json::from_str("3.15").unwrap(); - assert_eq!(deserialized, value); + #[test] + fn test_into_f64() { + let f: f64 = OrderedF64(2.5).into(); + assert_eq!(f, 2.5); } #[test] @@ -182,4 +210,26 @@ mod tests { let schema_str = serde_json::to_string_pretty(&schema).unwrap(); assert!(schema_str.contains("\"type\": \"number\"")); } + + #[test] + fn test_default() { + assert_eq!(*OrderedF64::default(), 0.0); + } + + #[test] + fn test_clone() { + let a = OrderedF64(2.5); + assert_eq!(a.clone(), a); + } + + #[test] + fn test_serialize() { + assert_eq!(serde_json::to_string(&OrderedF64(3.15)).unwrap(), "3.15"); + } + + #[test] + fn test_deserialize() { + let value: OrderedF64 = serde_json::from_str("3.15").unwrap(); + assert_eq!(value, OrderedF64(3.15)); + } } From d7421c2ce02ac8efc70a5e09d487fe15ce102b9e Mon Sep 17 00:00:00 2001 From: jerbly Date: Sat, 10 Jan 2026 13:06:54 -0500 Subject: [PATCH 4/5] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eff4b5c6..623f6646b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. - New Experimental feature: `weaver serve` command to serve a REST API and web UI. ([#1076](https://github.com/open-telemetry/weaver/pull/1076) by @jerbly) - Add support for diff schemas in `registry json-schema`([#1105](https://github.com/open-telemetry/weaver/pull/1105) by @lmolkova) +- 💥 BREAKING CHANGE 💥 Upgraded JSON schema generation to [2020-12 version of JSON Schema.](https://json-schema.org/draft/2020-12/schema) ([#1125](https://github.com/open-telemetry/weaver/pull/1125) by @jerbly) # [0.20.0] - 2025-12-11 From 93cf71fe3a4f7949fce32eb1b6f9ba200aa1bbec Mon Sep 17 00:00:00 2001 From: jerbly Date: Sat, 10 Jan 2026 13:07:42 -0500 Subject: [PATCH 5/5] fmt --- crates/weaver_common/src/ordered_float.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/weaver_common/src/ordered_float.rs b/crates/weaver_common/src/ordered_float.rs index 54e58dd6e..7d1a9d0a2 100644 --- a/crates/weaver_common/src/ordered_float.rs +++ b/crates/weaver_common/src/ordered_float.rs @@ -169,15 +169,24 @@ mod tests { #[test] fn test_partial_ord() { - assert_eq!(OrderedF64(1.0).partial_cmp(&OrderedF64(2.0)), Some(Ordering::Less)); + assert_eq!( + OrderedF64(1.0).partial_cmp(&OrderedF64(2.0)), + Some(Ordering::Less) + ); } #[test] fn test_ord() { assert_eq!(OrderedF64(1.0).cmp(&OrderedF64(2.0)), Ordering::Less); - assert_eq!(OrderedF64(f64::NAN).cmp(&OrderedF64(1.0)), Ordering::Greater); + assert_eq!( + OrderedF64(f64::NAN).cmp(&OrderedF64(1.0)), + Ordering::Greater + ); assert_eq!(OrderedF64(1.0).cmp(&OrderedF64(f64::NAN)), Ordering::Less); - assert_eq!(OrderedF64(f64::NAN).cmp(&OrderedF64(f64::NAN)), Ordering::Equal); + assert_eq!( + OrderedF64(f64::NAN).cmp(&OrderedF64(f64::NAN)), + Ordering::Equal + ); } #[test]