unc_rpc_error_macro/
lib.rs

1use std::cell::RefCell;
2use std::collections::BTreeMap;
3
4use proc_macro::TokenStream;
5use serde::{Deserialize, Serialize};
6#[cfg(feature = "dump_errors_schema")]
7use serde_json::Value;
8use syn::{parse_macro_input, DeriveInput};
9
10use unc_rpc_error_core::{parse_error_type, ErrorType};
11
12thread_local!(static SCHEMA: RefCell<Schema> = RefCell::new(Schema::default()));
13
14#[derive(Default, Debug, Deserialize, Serialize)]
15struct Schema {
16    pub schema: BTreeMap<String, ErrorType>,
17}
18
19#[cfg(feature = "dump_errors_schema")]
20fn merge(a: &mut Value, b: &Value) {
21    match (a, b) {
22        (&mut Value::Object(ref mut a), &Value::Object(ref b)) => {
23            for (k, v) in b {
24                merge(a.entry(k.clone()).or_insert(Value::Null), v);
25            }
26        }
27        (a, b) => {
28            *a = b.clone();
29        }
30    }
31}
32
33#[cfg(feature = "dump_errors_schema")]
34impl Drop for Schema {
35    /// `rpc_error` wants to collect **all** invocations of the macro across the
36    /// project and merge them into a single file. These kinds of macros are not
37    /// supported at all by Rust macro unc-infra.tructure, so we use gross hacks
38    /// here.
39    ///
40    /// Every macro invocation merges its results into the
41    /// rpc_errors_schema.json file, with the file playing the role of global
42    /// mutable state which can be accessed from different processes. To avoid
43    /// race conditions, we use file-locking. File locking isn't a very robust
44    /// thing, but it should ok be considering the level of hack here.
45    fn drop(&mut self) {
46        use fs2::FileExt;
47        use std::fs::File;
48        use std::io::{Read, Seek, SeekFrom, Write};
49
50        struct Guard {
51            file: File,
52        }
53        impl Guard {
54            fn new(path: &str) -> Self {
55                let file = File::options()
56                    .read(true)
57                    .write(true)
58                    .create_new(true)
59                    .open(path)
60                    .or_else(|_| File::options().read(true).write(true).open(path))
61                    .unwrap_or_else(|err| panic!("can't open {path}: {err}"));
62                file.lock_exclusive().unwrap_or_else(|err| panic!("can't lock {path}: {err}"));
63                Guard { file }
64            }
65        }
66        impl Drop for Guard {
67            fn drop(&mut self) {
68                let _ = self.file.unlock();
69            }
70        }
71
72        let schema_json = serde_json::to_value(self).expect("Schema serialize failed");
73
74        // std::env::var("CARGO_TARGET_DIR") doesn't exists
75        let filename = "./target/rpc_errors_schema.json";
76        let mut guard = Guard::new(filename);
77
78        let existing_schema: Option<Value> = {
79            let mut buf = Vec::new();
80            guard
81                .file
82                .read_to_end(&mut buf)
83                .unwrap_or_else(|err| panic!("can't read {filename}: {err}"));
84            if buf.is_empty() {
85                None
86            } else {
87                let json = serde_json::from_slice(&buf)
88                    .unwrap_or_else(|err| panic!("can't deserialize {filename}: {err}"));
89                Some(json)
90            }
91        };
92
93        let new_schema_json = match existing_schema {
94            None => schema_json,
95            Some(mut existing_schema) => {
96                merge(&mut existing_schema, &schema_json);
97                existing_schema
98            }
99        };
100
101        let new_schema_json_string = serde_json::to_string_pretty(&new_schema_json)
102            .expect("error schema serialization failed");
103
104        guard.file.set_len(0).unwrap_or_else(|err| panic!("can't truncate {filename}: {err}"));
105        guard
106            .file
107            .seek(SeekFrom::Start(0))
108            .unwrap_or_else(|err| panic!("can't seek {filename}: {err}"));
109        guard
110            .file
111            .write_all(new_schema_json_string.as_bytes())
112            .unwrap_or_else(|err| panic!("can't write {filename}: {err}"));
113    }
114}
115
116#[proc_macro_derive(RpcError)]
117pub fn rpc_error(input: TokenStream) -> TokenStream {
118    let input = parse_macro_input!(input as DeriveInput);
119
120    SCHEMA.with(|s| {
121        parse_error_type(&mut s.borrow_mut().schema, &input);
122    });
123    TokenStream::new()
124}