1use 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
41pub struct StandardsMetadata {
42 pub targets: BTreeMap<String, TargetStandards>,
47}
48
49impl StandardsMetadata {
50 #[must_use]
53 pub fn is_empty(&self) -> bool {
54 self.targets.is_empty()
55 }
56
57 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub struct TargetStandards {
114 pub header_only: bool,
119 pub gnu_extensions: bool,
123 pub interface_c: Requirement<CStandard>,
126 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
141impl 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#[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#[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 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 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" => {
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 #[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 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 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 #[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 #[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 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 #[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 let parsed: StandardsMetadata = serde_json::from_value(to_json(&metadata)).unwrap();
541 assert_eq!(parsed, metadata);
542 }
543
544 #[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 #[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 let parsed: StandardsMetadata = serde_json::from_value(serde_json::json!({})).unwrap();
593 assert_eq!(parsed, metadata);
594 }
595
596 #[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 #[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 #[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 #[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}