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;