sofie_model/
contract_traits.rs1use std::path::{Path, PathBuf};
2
3use syn::visit::Visit;
4use syn::{FnArg, GenericParam, Item, ReceiverKind, ReturnType, Signature, TraitItem, Type};
5
6use crate::syntax::{first_type_argument, has_attribute, line_of};
7
8#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct ContractTrait {
11 pub name: String,
12 pub path: Vec<String>,
14 pub methods: Vec<ContractMethod>,
15 pub file: PathBuf,
16 pub line: usize,
17}
18
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct ContractMethod {
21 pub name: String,
22 pub stays_local: Option<&'static str>,
24}
25
26impl ContractTrait {
27 #[must_use]
28 pub fn crosses_apps(&self) -> bool {
29 self.methods
30 .iter()
31 .all(|method| method.stays_local.is_none())
32 }
33}
34
35pub(crate) fn contract_trait(
36 item: &Item,
37 file: &Path,
38 module_path: &[String],
39) -> Option<ContractTrait> {
40 let Item::Trait(contract) = item else {
41 return None;
42 };
43 if !has_attribute(&contract.attrs, "contract") {
44 return None;
45 }
46 let methods = contract
47 .items
48 .iter()
49 .filter_map(|item| match item {
50 TraitItem::Fn(function) => Some(ContractMethod {
51 name: function.sig.ident.to_string(),
52 stays_local: stays_local(&function.sig),
53 }),
54 _ => None,
55 })
56 .collect();
57 Some(ContractTrait {
58 name: contract.ident.to_string(),
59 path: [module_path, &[contract.ident.to_string()]].concat(),
60 methods,
61 file: file.to_path_buf(),
62 line: line_of(item),
63 })
64}
65
66fn stays_local(signature: &Signature) -> Option<&'static str> {
68 if signature.asyncness.is_none() {
69 return Some("it is not `async`");
70 }
71 if has_type_parameters(signature) {
72 return Some("it is generic");
73 }
74 if !signature.inputs.iter().all(input_crosses) {
75 return Some("it takes `&mut`, `self` by value or a type holding a reference");
76 }
77 if !answers_owned_result(&signature.output) {
78 return Some("it does not answer `Result<…>` of an owned type");
79 }
80 None
81}
82
83fn has_type_parameters(signature: &Signature) -> bool {
84 signature
85 .generics
86 .params
87 .iter()
88 .any(|parameter| !matches!(parameter, GenericParam::Lifetime(_)))
89}
90
91fn input_crosses(input: &FnArg) -> bool {
93 match input {
94 FnArg::Receiver(receiver) => matches!(receiver.kind, ReceiverKind::Reference(_, _, None)),
95 FnArg::Typed(typed) => match typed.ty.as_ref() {
96 Type::Reference(reference) => reference.mutability.is_none() && owned(&reference.elem),
97 other => owned(other),
98 },
99 }
100}
101
102fn answers_owned_result(output: &ReturnType) -> bool {
103 let ReturnType::Type(_, output) = output else {
104 return false;
105 };
106 let Type::Path(path) = output.as_ref() else {
107 return false;
108 };
109 path.path
110 .segments
111 .last()
112 .is_some_and(|last| last.ident == "Result" && first_type_argument(last).is_some_and(owned))
113}
114
115fn owned(ty: &Type) -> bool {
116 let mut borrows = Borrows(false);
117 borrows.visit_type(ty);
118 !borrows.0
119}
120
121struct Borrows(bool);
123
124impl Visit<'_> for Borrows {
125 fn visit_lifetime(&mut self, _lifetime: &syn::Lifetime) {
126 self.0 = true;
127 }
128
129 fn visit_type_reference(&mut self, _reference: &syn::TypeReference) {
130 self.0 = true;
131 }
132
133 fn visit_path_segment(&mut self, segment: &syn::PathSegment) {
134 self.0 |= segment.ident == "Self";
135 syn::visit::visit_path_segment(self, segment);
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn read(source: &str) -> ContractTrait {
144 let item: Item = syn::parse_str(source).unwrap();
145 contract_trait(
146 &item,
147 Path::new("contract/src/lib.rs"),
148 &["orders".to_owned()],
149 )
150 .unwrap()
151 }
152
153 #[test]
154 fn a_contract_crosses_apps_only_when_every_method_can() {
155 let remote = read(
156 "#[sofie::contract] pub trait Orders: Send + Sync {
157 async fn total(&self, order: Uuid, names: &[String], note: &str) -> Result<Money>;
158 }",
159 );
160 let local = read(
161 "#[contract] pub trait Ledger {
162 async fn rewrite(&self, entries: &mut Vec<u8>) -> Result<()>;
163 fn now(&self) -> Result<i64>;
164 async fn borrowed(&self) -> Result<&'static str>;
165 }",
166 );
167
168 assert_eq!(remote.path, ["orders", "Orders"]);
169 assert!(remote.crosses_apps());
170 let reasons: Vec<Option<&str>> = local
171 .methods
172 .iter()
173 .map(|method| method.stays_local)
174 .collect();
175 assert_eq!(
176 reasons,
177 [
178 Some("it takes `&mut`, `self` by value or a type holding a reference"),
179 Some("it is not `async`"),
180 Some("it does not answer `Result<…>` of an owned type"),
181 ]
182 );
183 }
184
185 #[test]
186 fn an_unmarked_trait_is_no_contract() {
187 let item: Item = syn::parse_str("pub trait Plain {}").unwrap();
188
189 assert!(contract_trait(&item, Path::new("lib.rs"), &[]).is_none());
190 }
191}