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;