Skip to main content

api_parity_rs/
lib.rs

1//! Runtime types for the api-parity-rs port plugin.
2//!
3//! # How it works
4//!
5//! 1. Source code is annotated with `#[parity_impl]` (on `impl` blocks)
6//!    or `#[parity(...)]` (on free functions, structs, enums, or type
7//!    aliases). Those macros live in
8//!    `api-parity-rs-macros` and are re-exported below.
9//! 2. Each annotation expands to an `inventory::submit! { ParityEntry { ... } }`
10//!    call. The `inventory` crate uses link-time registration: each
11//!    `submit!` drops a static into a special section, and
12//!    `inventory::iter::<T>()` walks them at runtime. No central registry,
13//!    no init order, and the stub fn the attribute is attached to never
14//!    has to be called for the entry to be registered.
15//! 3. A target-crate binary calls `dump_to_writer` (gated on the `serde`
16//!    feature) to serialize the registered entries as a `kind=port`
17//!    envelope (per `SCHEMA.md`) for `api-parity` to consume.
18//!
19//! The crate is intentionally domain-agnostic: `ParityEntry::path` is
20//! just an opaque string. It can name a PySpark API, a REST endpoint, etc.
21
22// Re-exported so the macros can refer to `::api_parity_rs::inventory::submit!`
23// without users having to add `inventory` as a direct dependency.
24pub use inventory;
25pub use api_parity_rs_macros::{parity, parity_impl};
26
27#[cfg(feature = "walker")]
28pub mod walk;
29
30/// Implementation state of a tracked API.
31///
32/// `Unimplemented` is special-cased by the macros: it requires a `comment`
33/// explaining *why* the stub exists, so reviewers get context at the call site.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum Status {
36    Implemented,
37    Partial,
38    Unimplemented,
39}
40
41impl Status {
42    pub fn as_str(&self) -> &'static str {
43        match self {
44            Status::Implemented => "implemented",
45            Status::Partial => "partial",
46            Status::Unimplemented => "unimplemented",
47        }
48    }
49}
50
51/// One row in the port-side inventory. All fields are `&'static str` so
52/// the struct can be built in `inventory::submit!` (which requires a
53/// `const`-constructible value).
54#[derive(Debug)]
55pub struct ParityEntry {
56    /// Reference path being mirrored (e.g. `"pyspark.sql.session.SparkSession.sql"`).
57    /// Must match a `path` on the reference side for the join to count.
58    pub path: &'static str,
59    /// Local symbol path (e.g. `"SparkSession::sql"`). Built by the macros
60    /// from `Self` + fn name (impl block) or `module_path!() ++ "::" ++ fn`
61    /// (free fn).
62    pub implementation: &'static str,
63    pub status: Status,
64    /// Opaque version string set by the user (e.g. `"3.5"`). Not interpreted
65    /// by this crate.
66    pub since: Option<&'static str>,
67    /// Free-form note. Required when `status == Unimplemented`.
68    pub comment: Option<&'static str>,
69    /// Tracker issue number (e.g. GitHub issue #42).
70    pub issue: Option<u32>,
71}
72
73// Tells `inventory` that `ParityEntry` is a collected type; this is what
74// makes `inventory::iter::<ParityEntry>()` work in downstream binaries.
75inventory::collect!(ParityEntry);
76
77#[cfg(feature = "serde")]
78mod dump {
79    //! JSON serialization, gated on `serde` so target crates that just
80    //! want to register entries don't pay the serde compile cost.
81
82    use super::*;
83    use serde::Serialize;
84    use std::io::Write;
85
86    /// Wire-format DTO for a single entry (matches SCHEMA.md `port` shape).
87    /// Local to this module so the public `ParityEntry` stays serde-free.
88    #[derive(Serialize)]
89    struct EntryDto<'a> {
90        path: &'a str,
91        implementation: &'a str,
92        status: &'a str,
93        since: Option<&'a str>,
94        issue: Option<u32>,
95        comment: Option<&'a str>,
96    }
97
98    /// Wire-format envelope. `schema_version` is bumped when the contract
99    /// breaks; `kind` is hard-coded to `"port"` because that's the only
100    /// thing this crate produces.
101    #[derive(Serialize)]
102    struct Envelope<'a> {
103        schema_version: u32,
104        kind: &'a str,
105        language: &'a str,
106        version: &'a str,
107        source: &'a str,
108        entries: Vec<EntryDto<'a>>,
109    }
110
111    /// Serialize all registered `ParityEntry` values as a `kind=port`
112    /// envelope (per SCHEMA.md) to `out`.
113    ///
114    /// Typical usage from a target crate:
115    ///
116    /// ```ignore
117    /// fn main() {
118    ///     api_parity_rs::dump_to_writer(
119    ///         env!("CARGO_PKG_NAME"),
120    ///         env!("CARGO_PKG_VERSION"),
121    ///         std::io::stdout(),
122    ///     ).unwrap();
123    /// }
124    /// ```
125    pub fn dump_to_writer<W: Write>(
126        source: &str,
127        version: &str,
128        mut out: W,
129    ) -> Result<(), std::io::Error> {
130        // Sort by `path` for stable output — the JSON is part of the
131        // contract, and stability matters when diffing two versions.
132        let mut entries: Vec<&ParityEntry> = inventory::iter::<ParityEntry>.into_iter().collect();
133        entries.sort_by_key(|e| e.path);
134
135        let envelope = Envelope {
136            schema_version: 1,
137            kind: "port",
138            language: "rust",
139            version,
140            source,
141            entries: entries
142                .iter()
143                .map(|e| EntryDto {
144                    path: e.path,
145                    implementation: e.implementation,
146                    status: e.status.as_str(),
147                    since: e.since,
148                    issue: e.issue,
149                    comment: e.comment,
150                })
151                .collect(),
152        };
153
154        // `serde_json::Error` doesn't impl `Into<std::io::Error>`, so we
155        // do the conversion manually to keep the public signature clean.
156        let s = serde_json::to_string_pretty(&envelope)
157            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
158        writeln!(out, "{s}")
159    }
160}
161
162#[cfg(feature = "serde")]
163pub use dump::dump_to_writer;