1use crate::{ProtocolItem, TypeRef, ValueKind};
5use bern_protocol::format::{self, Use};
6use bern_protocol::{
7 CommandPolicy, CommandSpec, Concurrency, DESCRIPTOR_VERSION, EnumDef, EventSpec, ExternalNames,
8 FieldDef, LocalizedText, OperationKind, ParamSpec, ProtocolDescriptor, RecordDef, Retry, Scope,
9 StateField, Type, TypeTable, VariantDef,
10};
11use proc_macro2::Span;
12
13pub fn resolve(ty: &TypeRef, types: &TypeTable, span: Span) -> syn::Result<Type> {
15 Ok(match ty {
16 TypeRef::Unit => Type::Unit,
17 TypeRef::Bool => Type::Bool,
18 TypeRef::Int => Type::Int,
19 TypeRef::Float => Type::Float,
20 TypeRef::Text => Type::Text,
21 TypeRef::Optional(inner) => Type::optional(resolve(inner, types, span)?),
22 TypeRef::List(inner) => Type::list(resolve(inner, types, span)?),
23 TypeRef::Named(path) => {
24 let name = path.last().unwrap();
25 if path.len() > 1 {
26 return Err(syn::Error::new(
27 span,
28 format!(
29 "refer to `{name}` by its name; protocol types are declared in the protocol module"
30 ),
31 ));
32 }
33 if types.enum_def(name).is_some() {
34 Type::Enum { name: name.clone() }
35 } else if types.record_def(name).is_some() {
36 Type::Record { name: name.clone() }
37 } else {
38 return Err(syn::Error::new(
39 span,
40 format!(
41 "type `{name}` is not declared in this protocol; declare it here \
42 with `#[value]`, or depend on the module whose protocol declares it"
43 ),
44 ));
45 }
46 }
47 })
48}
49
50pub fn is_text(ty: &Type) -> bool {
52 match ty {
53 Type::Text => true,
54 Type::Optional { inner } | Type::List { inner } => is_text(inner),
55 _ => false,
56 }
57}
58
59pub fn content(field: &crate::FieldItem, ty: &Type) -> syn::Result<bool> {
61 if field.content && !is_text(ty) {
62 return Err(syn::Error::new(
63 field.span,
64 format!(
65 "`#[content]` marks text that is data, such as a title; `{}` is {}",
66 field.name,
67 ty.display()
68 ),
69 ));
70 }
71 Ok(field.content)
72}
73
74pub fn is_date(ty: &Type) -> bool {
76 matches!(ty, Type::Record { name } if name == format::CALENDAR_DATE)
77}
78
79pub fn label(
83 enum_name: &str,
84 variant: &str,
85 tr: &crate::Tr,
86 fields: &[FieldDef],
87) -> syn::Result<LocalizedText> {
88 let mut params: Vec<String> = Vec::new();
89 for entry in &tr.entries {
90 let pieces =
91 format::parse(&entry.text).map_err(|message| syn::Error::new(entry.span, message))?;
92 for (name, how) in format::uses(&pieces) {
93 let Some(field) = fields.iter().find(|field| field.name == name) else {
94 return Err(syn::Error::new(
95 entry.span,
96 format!(
97 "`{{{name}}}` is not a field of `{enum_name}::{variant}`; a label shows the variant's own fields"
98 ),
99 ));
100 };
101 let fits = match how {
102 Use::Value => match &field.ty {
103 Type::Text => {
104 if !field.content {
105 return Err(syn::Error::new(
106 entry.span,
107 format!(
108 "the label of `{enum_name}::{variant}` shows `{name}`, text without translations; mark the field `#[content]` if it is data such as a title, or make it a labelled enum"
109 ),
110 ));
111 }
112 true
113 }
114 Type::Int | Type::Float | Type::Enum { .. } => true,
115 _ => false,
116 },
117 Use::Plural | Use::Duration => matches!(field.ty, Type::Int | Type::Float),
118 Use::Date => is_date(&field.ty),
119 };
120 if !fits {
121 return Err(syn::Error::new(
122 entry.span,
123 format!(
124 "`{name}` of `{enum_name}::{variant}` is {}, which `{{{name}{}}}` cannot show",
125 field.ty.display(),
126 match how {
127 Use::Value => String::new(),
128 other => format!(", {}, …", other.name()),
129 }
130 ),
131 ));
132 }
133 if !params.contains(&name) {
134 params.push(name);
135 }
136 }
137 }
138 Ok(LocalizedText {
139 key: bern_protocol::label_key(enum_name, variant),
140 translations: tr
141 .entries
142 .iter()
143 .map(|entry| (entry.language.clone(), entry.text.clone()))
144 .collect(),
145 params,
146 })
147}
148
149fn protocol_types(protocol: &ProtocolItem) -> Vec<&TypeRef> {
151 let mut types = Vec::new();
152 for value in &protocol.values {
153 match &value.kind {
154 ValueKind::Enum(variants) => {
155 for variant in variants {
156 types.extend(variant.fields.iter().map(|field| &field.ty));
157 }
158 }
159 ValueKind::Record(fields) => types.extend(fields.iter().map(|field| &field.ty)),
160 }
161 }
162 types.extend(protocol.state.fields.iter().map(|field| &field.ty));
163 for method in &protocol.commands.methods {
164 types.extend(method.params.iter().map(|param| ¶m.ty));
165 types.push(&method.result);
166 }
167 for method in protocol.events.iter().flat_map(|events| &events.methods) {
168 types.extend(method.params.iter().map(|param| ¶m.ty));
169 }
170 for method in protocol.queries.iter().flat_map(|queries| &queries.methods) {
171 types.extend(method.params.iter().map(|param| ¶m.ty));
172 types.push(&method.result);
173 }
174 types
175}
176
177pub fn protocol_descriptor(protocol: &ProtocolItem) -> syn::Result<ProtocolDescriptor> {
179 protocol_descriptor_with(protocol, &TypeTable::default())
180}
181
182fn named_types<'a>(ty: &'a Type, names: &mut Vec<&'a str>) {
184 match ty {
185 Type::Record { name } | Type::Enum { name } => names.push(name),
186 Type::Optional { inner } | Type::List { inner } => named_types(inner, names),
187 _ => {}
188 }
189}
190
191pub fn protocol_descriptor_with(
196 protocol: &ProtocolItem,
197 imported: &TypeTable,
198) -> syn::Result<ProtocolDescriptor> {
199 let mut types = TypeTable::default();
201 for value in &protocol.values {
202 for framework in [bern_media::MEDIA_SOURCE, format::CALENDAR_DATE] {
203 if value.name == framework {
204 return Err(syn::Error::new(
205 value.span,
206 format!(
207 "`{framework}` is a framework type; use it from `bern::prelude` instead of declaring it"
208 ),
209 ));
210 }
211 }
212 }
213 let used = protocol_types(protocol);
215 if used
216 .iter()
217 .any(|ty| crate::mentions(ty, bern_media::MEDIA_SOURCE))
218 {
219 types.records.push(bern_media::media_source_def());
220 }
221 if used
222 .iter()
223 .any(|ty| crate::mentions(ty, format::CALENDAR_DATE))
224 {
225 types.records.push(format::calendar_date_def());
226 }
227 for value in &protocol.values {
228 match &value.kind {
229 ValueKind::Enum(_) => types.enums.push(EnumDef {
230 name: value.name.to_string(),
231 variants: vec![],
232 }),
233 ValueKind::Record(_) => types.records.push(RecordDef {
234 name: value.name.to_string(),
235 fields: vec![],
236 }),
237 }
238 }
239 let mut wanted: Vec<String> = Vec::new();
241 for ty in &used {
242 crate::named(ty, &mut |name| wanted.push(name.to_string()));
243 }
244 let mut next = 0;
245 while next < wanted.len() {
246 let name = wanted[next].clone();
247 next += 1;
248 if types.enum_def(&name).is_some() || types.record_def(&name).is_some() {
249 continue;
250 }
251 let mut uses = Vec::new();
252 if let Some(def) = imported.enum_def(&name) {
253 for field in def.variants.iter().flat_map(|variant| &variant.fields) {
254 named_types(&field.ty, &mut uses);
255 }
256 types.enums.push(def.clone());
257 } else if let Some(def) = imported.record_def(&name) {
258 for field in &def.fields {
259 named_types(&field.ty, &mut uses);
260 }
261 types.records.push(def.clone());
262 }
263 wanted.extend(uses.into_iter().map(str::to_string));
264 }
265 let lookup = types.clone();
266 for value in &protocol.values {
267 let name = value.name.to_string();
268 match &value.kind {
269 ValueKind::Enum(variants) => {
270 let mut defs = Vec::new();
271 for variant in variants {
272 let mut fields = Vec::new();
273 for field in &variant.fields {
274 let ty = resolve(&field.ty, &lookup, field.span)?;
275 fields.push(FieldDef {
276 name: field.name.to_string(),
277 content: content(field, &ty)?,
278 ty,
279 doc: field.doc.clone(),
280 });
281 }
282 let text = match &variant.label {
283 Some(tr) => Some(label(&name, &variant.name.to_string(), tr, &fields)?),
284 None => None,
285 };
286 defs.push(VariantDef {
287 name: variant.name.to_string(),
288 fields,
289 label: text,
290 });
291 }
292 types
293 .enums
294 .iter_mut()
295 .find(|def| def.name == name)
296 .unwrap()
297 .variants = defs;
298 }
299 ValueKind::Record(fields) => {
300 let mut defs = Vec::new();
301 for field in fields {
302 let ty = resolve(&field.ty, &lookup, field.span)?;
303 defs.push(FieldDef {
304 name: field.name.to_string(),
305 content: content(field, &ty)?,
306 ty,
307 doc: field.doc.clone(),
308 });
309 }
310 types
311 .records
312 .iter_mut()
313 .find(|def| def.name == name)
314 .unwrap()
315 .fields = defs;
316 }
317 }
318 }
319 let mut state = Vec::new();
320 for field in &protocol.state.fields {
321 let ty = resolve(&field.ty, &types, field.span)?;
322 state.push(StateField {
323 name: field.name.to_string(),
324 content: content(field, &ty)?,
325 ty,
326 doc: field.doc.clone(),
327 });
328 }
329 let namespace = protocol.args.namespace.clone();
330 let mut commands = Vec::new();
331 for method in &protocol.commands.methods {
332 let name = method.name.to_string();
333 let mut params = Vec::new();
334 for param in &method.params {
335 params.push(ParamSpec {
336 name: param.name.to_string(),
337 ty: resolve(¶m.ty, &types, param.name.span())?,
338 doc: String::new(),
339 });
340 }
341 commands.push(CommandSpec {
342 id: format!("{namespace}.{name}"),
343 external: ExternalNames::derive(&namespace, &name),
344 reads: vec![],
345 on: method
346 .on
347 .as_ref()
348 .map(|(module, event)| format!("{module}.{event}")),
349 name,
350 doc: method.doc.clone(),
351 kind: OperationKind::Command,
352 scope: Scope::Engine,
353 params,
354 result: resolve(&method.result, &types, method.span)?,
355 errors: method.errors.clone(),
356 policy: CommandPolicy {
357 retry: if method.idempotent {
358 Retry::Idempotent
359 } else {
360 Retry::Never
361 },
362 ..CommandPolicy::default()
363 },
364 });
365 }
366 for method in protocol.queries.iter().flat_map(|queries| &queries.methods) {
369 let name = method.name.to_string();
370 let mut params = Vec::new();
371 for param in &method.params {
372 params.push(ParamSpec {
373 name: param.name.to_string(),
374 ty: resolve(¶m.ty, &types, param.name.span())?,
375 doc: String::new(),
376 });
377 }
378 let mut reads = Vec::new();
379 for field in &method.reads {
380 if !state.iter().any(|state| *field == state.name) {
381 return Err(syn::Error::new(
382 field.span(),
383 format!(
384 "`{field}` is not a field of the protocol state; `#[reads(…)]` names \
385 the state fields the query's answer depends on"
386 ),
387 ));
388 }
389 reads.push(field.to_string());
390 }
391 commands.push(CommandSpec {
392 id: format!("{namespace}.{name}"),
393 external: ExternalNames::derive(&namespace, &name),
394 reads,
395 on: None,
396 name,
397 doc: method.doc.clone(),
398 kind: OperationKind::Query,
399 scope: Scope::Engine,
400 params,
401 result: resolve(&method.result, &types, method.span)?,
402 errors: method.errors.clone(),
403 policy: CommandPolicy {
406 concurrency: Concurrency::Overlapping,
407 retry: Retry::Idempotent,
408 cancellable: true,
409 },
410 });
411 }
412 let mut events = Vec::new();
413 for event in protocol.events.iter().flat_map(|events| &events.methods) {
414 let mut params = Vec::new();
415 for param in &event.params {
416 params.push(ParamSpec {
417 name: param.name.to_string(),
418 ty: resolve(¶m.ty, &types, param.name.span())?,
419 doc: String::new(),
420 });
421 }
422 events.push(EventSpec {
423 name: event.name.to_string(),
424 doc: event.doc.clone(),
425 params,
426 });
427 }
428 let descriptor = ProtocolDescriptor {
429 modules: Vec::new(),
430 events,
431 descriptor_version: DESCRIPTOR_VERSION,
432 namespace,
433 version: protocol.args.version,
434 types,
435 state,
436 commands,
437 };
438 descriptor
439 .validate()
440 .map_err(|error| syn::Error::new(protocol.args.span, error.0))?;
441 Ok(descriptor)
442}
443
444pub fn check_translations(
449 tr: &crate::Tr,
450 languages: Option<(&[String], bool)>,
451) -> Vec<(Span, String)> {
452 let mut problems = check_languages(tr, languages);
453 problems.extend(check_arguments(tr));
454 problems
455}
456
457pub fn check_languages(
461 tr: &crate::Tr,
462 languages: Option<(&[String], bool)>,
463) -> Vec<(Span, String)> {
464 use std::collections::BTreeSet;
465 let mut problems = Vec::new();
466 let mut seen = BTreeSet::new();
467 for entry in &tr.entries {
468 if !seen.insert(entry.language.as_str()) {
469 problems.push((
470 entry.span,
471 format!(
472 "error[B101]: language `{}` is translated twice",
473 entry.language
474 ),
475 ));
476 }
477 if let Some((supported, _)) = languages
478 && !supported.contains(&entry.language)
479 {
480 problems.push((
481 entry.span,
482 format!(
483 "error[B101]: language tag `{}` is not in [languages].supported ({}); check the spelling",
484 entry.language,
485 supported.join(", ")
486 ),
487 ));
488 }
489 }
490 if let Some((supported, true)) = languages {
491 let missing: Vec<_> = supported
492 .iter()
493 .filter(|language| !seen.contains(language.as_str()))
494 .cloned()
495 .collect();
496 if !missing.is_empty() {
497 problems.push((
498 tr.span,
499 format!(
500 "error[B102]: Missing translation\nMissing language: {}\nRequired languages: {}",
501 missing.join(", "),
502 supported.join(", ")
503 ),
504 ));
505 }
506 }
507 for entry in &tr.entries {
508 if let Ok(pieces) = format::parse(&entry.text) {
509 let missing = format::missing_plurals(&pieces, &entry.language);
510 if !missing.is_empty() {
511 problems.push((
512 entry.span,
513 format!(
514 "error[B104]: the `{}` text needs the plural branch {}, a form every number in the language takes",
515 entry.language,
516 missing.join(", ")
517 ),
518 ));
519 }
520 }
521 }
522 problems
523}
524
525fn check_arguments(tr: &crate::Tr) -> Vec<(Span, String)> {
528 use std::collections::BTreeSet;
529 let mut problems = Vec::new();
530 let declared: BTreeSet<String> = tr.args.iter().map(|arg| arg.name.to_string()).collect();
531 let mut used = BTreeSet::new();
532 let mut reference: Option<BTreeSet<String>> = None;
533 for entry in &tr.entries {
534 match LocalizedText::placeholders(&entry.text) {
535 Err(message) => problems.push((entry.span, format!("error[B104]: {message}"))),
536 Ok(names) => {
537 let names: BTreeSet<String> = names.into_iter().collect();
538 for name in &names {
539 if !declared.contains(name) {
540 problems.push((
541 entry.span,
542 format!(
543 "error[B104]: placeholder `{{{name}}}` has no argument; add `; {name}: value`"
544 ),
545 ));
546 }
547 }
548 match &reference {
549 None => reference = Some(names.clone()),
550 Some(first) if *first != names => problems.push((
551 entry.span,
552 "error[B104]: every translation must use the same placeholders".to_string(),
553 )),
554 _ => {}
555 }
556 used.extend(names);
557 }
558 }
559 }
560 for arg in &tr.args {
561 if !used.contains(&arg.name.to_string()) {
562 problems.push((
563 arg.name.span(),
564 format!(
565 "error[B104]: argument `{}` is not used by any translation",
566 arg.name
567 ),
568 ));
569 }
570 }
571 problems
572}