Skip to main content

cabin_core/
index_standards.rs

1//! Published-index standard-compatibility metadata (the `standards`
2//! table of `docs/design/standard-compatibility/registry-index.md`).
3//!
4//! `cabin publish` derives the **declared** per-target requirement
5//! table from the manifest and stores it in each version's index
6//! entry, so index consumers - preference mode and publish lints - can
7//! read a candidate version's per-target interface requirements without
8//! downloading its source archive.  The stored cells are the declared
9//! per-target `ReqOf` of spec D9 (header-only inference applied); the
10//! transitive composition `R_L` (spec D10) depends on which dependency
11//! version a resolution picks, so consumers compose - the publisher
12//! stores declarations, not effective requirements.
13//!
14//! Absence encodes `unconstrained` at every granularity (whole table,
15//! target row, or language key), so every pre-`standards` index entry
16//! is a valid instance of this schema unchanged.  The field is additive
17//! and the index document stays `schema = 1`; loaders must accept it
18//! because the index rejects unknown fields.
19
20use std::collections::BTreeMap;
21use std::marker::PhantomData;
22
23use serde::de::{self, MapAccess, Visitor};
24use serde::ser::SerializeMap;
25use serde::{Deserialize, Deserializer, Serialize, Serializer};
26
27use crate::language_standard::{
28    CStandard, CxxStandard, effective_gnu_extensions, resolve_language_standards,
29};
30use crate::model::Package;
31use crate::standard_compatibility::{
32    EffectiveRequirements, Requirement, dependency_attributes, req_of_c, req_of_cxx,
33};
34
35/// The `standards` table for one package version: the declared
36/// per-target interface requirement table plus the per-target flags
37/// index consumers need.  Keyed by library-like target name in a
38/// `BTreeMap` for deterministic, sorted output.  An empty table is
39/// the same as absence: everything unconstrained.
40#[derive(Debug, Clone, Default, PartialEq, Eq)]
41pub struct StandardsMetadata {
42    /// Per-target rows, keyed by target name.  `cabin publish` writes
43    /// one entry per library-like target of the version (`library` and
44    /// `header-only` kinds); executables, tests, and examples never
45    /// constrain consumers and are omitted.
46    pub targets: BTreeMap<String, TargetStandards>,
47}
48
49impl StandardsMetadata {
50    /// Whether the table carries no rows.  An empty table is omitted
51    /// from the serialized index entry (absence = unconstrained).
52    #[must_use]
53    pub fn is_empty(&self) -> bool {
54        self.targets.is_empty()
55    }
56
57    /// The version-wide effective requirement: per language, the join
58    /// (spec D4) over every published target row's declared interface
59    /// requirement.  An absent or all-unconstrained table joins to
60    /// `unconstrained`.
61    ///
62    /// This is the candidate-version requirement that preference mode
63    /// checks a consumer against when it lacks per-edge target scoping
64    /// (the version-wide fallback described in section 1 of
65    /// `docs/design/standard-compatibility/preference-mode.md`).  It can
66    /// only over-constrain (a stricter `extras` target the consumer
67    /// never links still counts), which is a preference-only lossiness
68    /// the post-resolution validation corrects.
69    #[must_use]
70    pub fn version_wide_join(&self) -> EffectiveRequirements {
71        EffectiveRequirements {
72            c: Requirement::join_all(self.targets.values().map(|row| row.interface_c)),
73            cxx: Requirement::join_all(self.targets.values().map(|row| row.interface_cxx)),
74        }
75    }
76
77    /// Derive the declared per-target table from a resolved package.
78    ///
79    /// Every library-like target gets a row - even one whose
80    /// requirements are all unconstrained and whose flags are false
81    /// (the target existing and imposing nothing is itself
82    /// information).  Each cell is the target's declared `ReqOf`
83    /// (spec D9) for that language, computed through the shared
84    /// [`dependency_attributes`] mapping so the stored table matches
85    /// what the resolver-graph pass evaluates.
86    #[must_use]
87    pub fn from_package(package: &Package) -> Self {
88        let resolved = resolve_language_standards(&package.language);
89        let targets = package
90            .targets
91            .iter()
92            .filter(|target| target.kind.is_library_like())
93            .map(|target| {
94                let attributes = dependency_attributes(target, &resolved, &package.language);
95                let row = TargetStandards {
96                    header_only: target.kind.is_header_only(),
97                    gnu_extensions: effective_gnu_extensions(&package.language, target),
98                    interface_c: req_of_c(&attributes),
99                    interface_cxx: req_of_cxx(&attributes),
100                };
101                (target.name.as_str().to_owned(), row)
102            })
103            .collect();
104        Self { targets }
105    }
106}
107
108/// One target's row: the declared interface requirement for each
109/// language (spec D9's `ReqOf`, header-only inference applied) plus
110/// the two per-target flags.  A row whose requirements are all
111/// unconstrained and whose flags are false serializes as `{}`.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub struct TargetStandards {
114    /// Spec D6 `kind`: whether the target is header-only.  Never
115    /// enters the satisfaction predicate (D11 consumes only the
116    /// requirement); carried so index consumers know it without an
117    /// archive download.
118    pub header_only: bool,
119    /// The target's lowering-time GNU-dialect flag (spec D8,
120    /// invariant I1).  Never participates in compatibility; carried
121    /// so toolchain-aware tooling can surface per-target buildability.
122    pub gnu_extensions: bool,
123    /// Declared C interface requirement `ReqOf(t, C)` (spec D9).
124    /// `Unconstrained` is serialized as an omitted language key.
125    pub interface_c: Requirement<CStandard>,
126    /// Declared C++ interface requirement `ReqOf(t, C++)` (spec D9).
127    pub interface_cxx: Requirement<CxxStandard>,
128}
129
130impl Default for TargetStandards {
131    fn default() -> Self {
132        Self {
133            header_only: false,
134            gnu_extensions: false,
135            interface_c: Requirement::Unconstrained,
136            interface_cxx: Requirement::Unconstrained,
137        }
138    }
139}
140
141// ---------------------------------------------------------------------------
142// Wire format.  A dedicated shape (rather than reusing the manifest's
143// `InterfaceRequirement` serde) because the index cell omits the
144// reserved `max` on write and rejects a populated `max` on read, and
145// because absence of a cell means unconstrained.
146// ---------------------------------------------------------------------------
147
148impl Serialize for StandardsMetadata {
149    fn serialize<Ser: Serializer>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error> {
150        let raw = RawStandards {
151            targets: self
152                .targets
153                .iter()
154                .map(|(name, row)| (name.clone(), RawTarget::from(*row)))
155                .collect(),
156        };
157        raw.serialize(serializer)
158    }
159}
160
161impl<'de> Deserialize<'de> for StandardsMetadata {
162    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
163        let raw = RawStandards::deserialize(deserializer)?;
164        Ok(Self {
165            targets: raw
166                .targets
167                .into_iter()
168                .map(|(name, row)| (name, TargetStandards::from(row)))
169                .collect(),
170        })
171    }
172}
173
174#[derive(Serialize, Deserialize, Default)]
175#[serde(deny_unknown_fields)]
176struct RawStandards {
177    #[serde(default)]
178    targets: BTreeMap<String, RawTarget>,
179}
180
181#[derive(Serialize, Deserialize, Default)]
182#[serde(deny_unknown_fields)]
183struct RawTarget {
184    #[serde(
185        rename = "header-only",
186        default,
187        skip_serializing_if = "std::ops::Not::not"
188    )]
189    header_only: bool,
190    #[serde(
191        rename = "gnu-extensions",
192        default,
193        skip_serializing_if = "std::ops::Not::not"
194    )]
195    gnu_extensions: bool,
196    #[serde(default, skip_serializing_if = "RawInterface::is_empty")]
197    interface: RawInterface,
198}
199
200/// The `interface` sub-table: a language key (`"c"`, `"c++"`, in that
201/// fixed order) maps to a cell.  A missing key is unconstrained.
202#[derive(Serialize, Deserialize, Default)]
203#[serde(deny_unknown_fields)]
204struct RawInterface {
205    #[serde(rename = "c", default, skip_serializing_if = "Option::is_none")]
206    c: Option<Cell<CStandard>>,
207    #[serde(rename = "c++", default, skip_serializing_if = "Option::is_none")]
208    cxx: Option<Cell<CxxStandard>>,
209}
210
211impl RawInterface {
212    fn is_empty(&self) -> bool {
213        self.c.is_none() && self.cxx.is_none()
214    }
215}
216
217impl From<TargetStandards> for RawTarget {
218    fn from(row: TargetStandards) -> Self {
219        Self {
220            header_only: row.header_only,
221            gnu_extensions: row.gnu_extensions,
222            interface: RawInterface {
223                c: cell_of(row.interface_c),
224                cxx: cell_of(row.interface_cxx),
225            },
226        }
227    }
228}
229
230impl From<RawTarget> for TargetStandards {
231    fn from(raw: RawTarget) -> Self {
232        Self {
233            header_only: raw.header_only,
234            gnu_extensions: raw.gnu_extensions,
235            interface_c: requirement_of(raw.interface.c),
236            interface_cxx: requirement_of(raw.interface.cxx),
237        }
238    }
239}
240
241/// One serialized `(target, language)` cell: the literal string
242/// `"none"` for `Requirement::Forbidden`, or a `{ "min": "<level>" }`
243/// table for `Requirement::Min`.  `Requirement::Unconstrained` is
244/// never a cell - the language key is omitted instead - so this type
245/// only encodes the two constrained shapes, which are deliberately
246/// distinct wire forms (never the same requirement).
247#[derive(Debug, Clone, Copy)]
248enum Cell<S> {
249    Forbidden,
250    Min(S),
251}
252
253fn cell_of<S>(requirement: Requirement<S>) -> Option<Cell<S>> {
254    match requirement {
255        Requirement::Unconstrained => None,
256        Requirement::Min(min) => Some(Cell::Min(min)),
257        Requirement::Forbidden => Some(Cell::Forbidden),
258    }
259}
260
261fn requirement_of<S>(cell: Option<Cell<S>>) -> Requirement<S> {
262    match cell {
263        None => Requirement::Unconstrained,
264        Some(Cell::Min(min)) => Requirement::Min(min),
265        Some(Cell::Forbidden) => Requirement::Forbidden,
266    }
267}
268
269impl<S: Serialize> Serialize for Cell<S> {
270    fn serialize<Ser: Serializer>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error> {
271        match self {
272            // `max` is reserved and never written in v1 (spec D4
273            // remark), so a minimum serializes as a single-key table.
274            Cell::Forbidden => serializer.serialize_str("none"),
275            Cell::Min(min) => {
276                let mut map = serializer.serialize_map(Some(1))?;
277                map.serialize_entry("min", min)?;
278                map.end()
279            }
280        }
281    }
282}
283
284impl<'de, S: Deserialize<'de>> Deserialize<'de> for Cell<S> {
285    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
286        struct CellVisitor<S>(PhantomData<S>);
287
288        impl<'de, S: Deserialize<'de>> Visitor<'de> for CellVisitor<S> {
289            type Value = Cell<S>;
290
291            fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292                f.write_str(r#"`"none"` or a `{ "min": "<level>" }` requirement table"#)
293            }
294
295            // A bare level string (`"c++17"`) is not a valid cell:
296            // writers must use the object form for minima.
297            fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
298                if value == "none" {
299                    Ok(Cell::Forbidden)
300                } else {
301                    Err(E::invalid_value(
302                        de::Unexpected::Str(value),
303                        &r#"the string "none" or a `{ "min": "<level>" }` table (a bare standard string is not a valid requirement cell)"#,
304                    ))
305                }
306            }
307
308            fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
309                let mut min: Option<S> = None;
310                while let Some(key) = map.next_key::<String>()? {
311                    match key.as_str() {
312                        "min" => {
313                            if min.is_some() {
314                                return Err(de::Error::duplicate_field("min"));
315                            }
316                            min = Some(map.next_value()?);
317                        }
318                        // `max` is reserved for a future version (spec
319                        // D4 remark).  A populated `max` is rejected
320                        // with the same "range reserved" diagnostic the
321                        // manifest parser gives range-like inputs; an
322                        // explicit `null` is accepted as unpopulated.
323                        "max" => {
324                            let max: Option<S> = map.next_value()?;
325                            if max.is_some() {
326                                return Err(de::Error::custom(
327                                    "range requirements are reserved for a future version of Cabin; the `max` field of an interface requirement must not be set",
328                                ));
329                            }
330                        }
331                        other => {
332                            return Err(de::Error::unknown_field(other, &["min", "max"]));
333                        }
334                    }
335                }
336                match min {
337                    Some(min) => Ok(Cell::Min(min)),
338                    None => Err(de::Error::missing_field("min")),
339                }
340            }
341        }
342
343        deserializer.deserialize_any(CellVisitor(PhantomData))
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350    use crate::language_standard::{InterfaceRequirement, StandardRequirement};
351    use crate::model::{Package, PackageName, Target, TargetKind, TargetName};
352    use crate::{LanguageStandardSettings, StandardDeclaration};
353    use camino::Utf8PathBuf;
354
355    fn interface_min<S>(min: S) -> InterfaceRequirement<S> {
356        InterfaceRequirement::Requirement(StandardRequirement { min, max: None })
357    }
358
359    fn target(
360        name: &str,
361        kind: TargetKind,
362        sources: &[&str],
363        language: LanguageStandardSettings,
364    ) -> Target {
365        Target {
366            name: TargetName::new(name).unwrap(),
367            kind,
368            sources: sources.iter().map(Utf8PathBuf::from).collect(),
369            include_dirs: Vec::new(),
370            defines: Vec::new(),
371            deps: Vec::new(),
372            required_features: Vec::new(),
373            language,
374        }
375    }
376
377    fn package(targets: Vec<Target>, language: LanguageStandardSettings) -> Package {
378        let mut package = Package::new(
379            PackageName::new("demo").unwrap(),
380            semver_ver(),
381            targets,
382            Vec::new(),
383        )
384        .unwrap();
385        package.language = language;
386        package
387    }
388
389    fn semver_ver() -> semver::Version {
390        semver::Version::parse("1.0.0").unwrap()
391    }
392
393    fn to_json(metadata: &StandardsMetadata) -> serde_json::Value {
394        serde_json::to_value(metadata).unwrap()
395    }
396
397    /// A compiled C++ library declaring `interface-cxx-standard =
398    /// "c++17"` gets `c++` = min c++17 (D9 row 2) and `c` = `"none"`
399    /// (D9 row 6, the strict C++-to-C default written explicitly);
400    /// executables are omitted, and an undeclared library is `{}`.
401    #[test]
402    fn from_package_derives_declared_table() {
403        let lib = target(
404            "fmt",
405            TargetKind::Library,
406            &["src/fmt.cc"],
407            LanguageStandardSettings {
408                cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx20)),
409                interface_cxx_standard: Some(StandardDeclaration::Declared(interface_min(
410                    CxxStandard::Cxx17,
411                ))),
412                ..Default::default()
413            },
414        );
415        let bin = target(
416            "app",
417            TargetKind::Executable,
418            &["src/main.cc"],
419            LanguageStandardSettings {
420                cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx20)),
421                ..Default::default()
422            },
423        );
424        let metadata = StandardsMetadata::from_package(&package(
425            vec![lib, bin],
426            LanguageStandardSettings::default(),
427        ));
428
429        // Only the library-like target appears.
430        assert_eq!(metadata.targets.keys().collect::<Vec<_>>(), ["fmt"]);
431        let row = &metadata.targets["fmt"];
432        assert_eq!(row.interface_cxx, Requirement::Min(CxxStandard::Cxx17));
433        // Row 6: no C implementation, no C interface -> forbidden.
434        assert_eq!(row.interface_c, Requirement::Forbidden);
435        assert!(!row.header_only);
436        assert!(!row.gnu_extensions);
437
438        assert_eq!(
439            to_json(&metadata),
440            serde_json::json!({
441                "targets": {
442                    "fmt": { "interface": { "c": "none", "c++": { "min": "c++17" } } }
443                }
444            })
445        );
446    }
447
448    /// Header-only inference (D9 row 3): a header-only C++ library
449    /// with no interface declaration infers `c++` = min from its
450    /// implementation standard, and records the `header-only` flag.
451    #[test]
452    fn header_only_target_infers_and_flags() {
453        let header_only = target(
454            "hdr",
455            TargetKind::HeaderOnly,
456            &[],
457            LanguageStandardSettings {
458                cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx20)),
459                ..Default::default()
460            },
461        );
462        let metadata = StandardsMetadata::from_package(&package(
463            vec![header_only],
464            LanguageStandardSettings::default(),
465        ));
466        let row = &metadata.targets["hdr"];
467        assert!(row.header_only);
468        assert_eq!(row.interface_cxx, Requirement::Min(CxxStandard::Cxx20));
469        assert_eq!(
470            to_json(&metadata),
471            serde_json::json!({
472                "targets": {
473                    "hdr": {
474                        "header-only": true,
475                        "interface": { "c": "none", "c++": { "min": "c++20" } }
476                    }
477                }
478            })
479        );
480    }
481
482    /// A C library declaring `interface-c-standard` gets `c` = min
483    /// (D9 row 2), `c++` = unconstrained (D9 row 5, the permissive
484    /// C-to-C++ default, omitted), and its `gnu-extensions` flag is
485    /// carried.
486    #[test]
487    fn c_library_with_gnu_extensions() {
488        let lib = target(
489            "clib",
490            TargetKind::Library,
491            &["src/clib.c"],
492            LanguageStandardSettings {
493                c_standard: Some(StandardDeclaration::Declared(CStandard::C11)),
494                interface_c_standard: Some(StandardDeclaration::Declared(interface_min(
495                    CStandard::C11,
496                ))),
497                gnu_extensions: Some(true),
498                ..Default::default()
499            },
500        );
501        let metadata = StandardsMetadata::from_package(&package(
502            vec![lib],
503            LanguageStandardSettings::default(),
504        ));
505        let row = &metadata.targets["clib"];
506        assert_eq!(row.interface_c, Requirement::Min(CStandard::C11));
507        // Permissive C-to-C++ default: unconstrained, so no `c++` key.
508        assert_eq!(row.interface_cxx, Requirement::Unconstrained);
509        assert!(row.gnu_extensions);
510        assert_eq!(
511            to_json(&metadata),
512            serde_json::json!({
513                "targets": {
514                    "clib": {
515                        "gnu-extensions": true,
516                        "interface": { "c": { "min": "c11" } }
517                    }
518                }
519            })
520        );
521    }
522
523    /// An undeclared library-like target still gets a row, serialized
524    /// as `{}` - the target existing and imposing nothing is itself
525    /// information.  (Row 5 for C++, but row 6 makes C forbidden, so a
526    /// truly empty `{}` needs no declared C either; use a header-only
527    /// target with no standards at all, which the manifest layer would
528    /// reject in practice but the derivation handles.)
529    #[test]
530    fn unconstrained_row_serializes_as_empty_object() {
531        let mut metadata = StandardsMetadata::default();
532        metadata
533            .targets
534            .insert("lib".to_owned(), TargetStandards::default());
535        assert_eq!(
536            to_json(&metadata),
537            serde_json::json!({ "targets": { "lib": {} } })
538        );
539        // And it round-trips back to an all-unconstrained row.
540        let parsed: StandardsMetadata = serde_json::from_value(to_json(&metadata)).unwrap();
541        assert_eq!(parsed, metadata);
542    }
543
544    /// `version_wide_join` folds every row's declared requirement per
545    /// language (spec D4): the strictest `c++` minimum and the `"none"`
546    /// forbidden C cell both surface; an all-unconstrained table joins
547    /// to unconstrained.
548    #[test]
549    fn version_wide_join_takes_the_strictest_per_language() {
550        assert_eq!(
551            StandardsMetadata::default().version_wide_join(),
552            EffectiveRequirements {
553                c: Requirement::Unconstrained,
554                cxx: Requirement::Unconstrained,
555            }
556        );
557        let mut metadata = StandardsMetadata::default();
558        metadata.targets.insert(
559            "core".to_owned(),
560            TargetStandards {
561                interface_c: Requirement::Forbidden,
562                interface_cxx: Requirement::Min(CxxStandard::Cxx17),
563                ..Default::default()
564            },
565        );
566        metadata.targets.insert(
567            "extras".to_owned(),
568            TargetStandards {
569                interface_cxx: Requirement::Min(CxxStandard::Cxx20),
570                ..Default::default()
571            },
572        );
573        assert_eq!(
574            metadata.version_wide_join(),
575            EffectiveRequirements {
576                c: Requirement::Forbidden,
577                cxx: Requirement::Min(CxxStandard::Cxx20),
578            }
579        );
580    }
581
582    /// Absence of the whole table is the empty table.
583    #[test]
584    fn empty_table_round_trips() {
585        let metadata = StandardsMetadata::default();
586        assert!(metadata.is_empty());
587        assert_eq!(to_json(&metadata), serde_json::json!({ "targets": {} }));
588        let parsed: StandardsMetadata =
589            serde_json::from_value(serde_json::json!({ "targets": {} })).unwrap();
590        assert_eq!(parsed, metadata);
591        // A `standards` object may omit `targets` entirely.
592        let parsed: StandardsMetadata = serde_json::from_value(serde_json::json!({})).unwrap();
593        assert_eq!(parsed, metadata);
594    }
595
596    /// The three requirement shapes round-trip through JSON: omitted
597    /// key (unconstrained), `{min}` (minimum), and `"none"`
598    /// (forbidden).
599    #[test]
600    fn cells_round_trip() {
601        let mut metadata = StandardsMetadata::default();
602        metadata.targets.insert(
603            "lib".to_owned(),
604            TargetStandards {
605                header_only: false,
606                gnu_extensions: false,
607                interface_c: Requirement::Forbidden,
608                interface_cxx: Requirement::Min(CxxStandard::Cxx20),
609            },
610        );
611        let json = to_json(&metadata);
612        assert_eq!(
613            json,
614            serde_json::json!({
615                "targets": {
616                    "lib": { "interface": { "c": "none", "c++": { "min": "c++20" } } }
617                }
618            })
619        );
620        let parsed: StandardsMetadata = serde_json::from_value(json).unwrap();
621        assert_eq!(parsed, metadata);
622    }
623
624    /// A populated `max` is rejected with the reserved-range
625    /// diagnostic; an explicit `max: null` is accepted as unpopulated.
626    #[test]
627    fn populated_max_is_rejected() {
628        let err = serde_json::from_value::<StandardsMetadata>(serde_json::json!({
629            "targets": { "lib": { "interface": { "c++": { "min": "c++17", "max": "c++20" } } } }
630        }))
631        .unwrap_err();
632        assert!(
633            err.to_string().contains("reserved for a future version"),
634            "unexpected error: {err}"
635        );
636
637        let parsed: StandardsMetadata = serde_json::from_value(serde_json::json!({
638            "targets": { "lib": { "interface": { "c++": { "min": "c++17", "max": null } } } }
639        }))
640        .unwrap();
641        assert_eq!(
642            parsed.targets["lib"].interface_cxx,
643            Requirement::Min(CxxStandard::Cxx17)
644        );
645    }
646
647    /// A bare standard string is not a valid cell (writers must use
648    /// the object form for minima).
649    #[test]
650    fn bare_standard_string_is_rejected() {
651        let err = serde_json::from_value::<StandardsMetadata>(serde_json::json!({
652            "targets": { "lib": { "interface": { "c++": "c++17" } } }
653        }))
654        .unwrap_err();
655        assert!(
656            err.to_string().contains("bare standard string")
657                || err.to_string().contains("invalid value"),
658            "unexpected error: {err}"
659        );
660    }
661
662    /// Unknown fields anywhere in the table are rejected.
663    #[test]
664    fn unknown_fields_are_rejected() {
665        assert!(
666            serde_json::from_value::<StandardsMetadata>(serde_json::json!({
667                "targets": { "lib": { "surprise": true } }
668            }))
669            .is_err()
670        );
671        assert!(
672            serde_json::from_value::<StandardsMetadata>(serde_json::json!({
673                "targets": { "lib": { "interface": { "rust": { "min": "c++17" } } } }
674            }))
675            .is_err()
676        );
677    }
678}