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