1use anyhow::{Context, Result, bail};
8use serde::Deserialize;
9use std::collections::BTreeSet;
10use std::fmt::Write as _;
11use std::fs;
12use std::path::{Path, PathBuf};
13
14use super::msg_parser::{MsgField, MsgResolver, MsgTypeRef, SrvSpec};
15use super::proto_gen::proto_package_name;
16
17#[derive(Debug, Deserialize)]
18struct ContractToml {
19 contract: ContractMeta,
20 mode: ModeSpec,
21}
22
23#[derive(Debug, Deserialize)]
24struct ContractMeta {
25 id: String,
26 #[allow(dead_code)]
27 version: String,
28 #[allow(dead_code)]
29 kind: String,
30 idl: String,
36}
37
38#[derive(Debug, Deserialize)]
39struct ModeSpec {
40 #[serde(rename = "type")]
41 mode_type: String,
42}
43
44struct IdlRef<'a> {
48 path: &'a str,
50}
51
52pub fn collect_referenced_srvs(contracts_dir: &Path) -> Result<BTreeSet<(String, String)>> {
56 let paths = collect_tomls(contracts_dir)?;
57 let mut set = BTreeSet::new();
58 for p in paths {
59 let raw =
60 fs::read_to_string(&p).with_context(|| format!("read contract {}", p.display()))?;
61 let c: ContractToml =
62 toml::from_str(&raw).with_context(|| format!("parse TOML {}", p.display()))?;
63 let idl = c.contract.idl.trim();
64 match parse_idl_path(idl) {
65 Some((pkg, "srv", name)) => {
66 set.insert((pkg.to_string(), name.to_string()));
67 }
68 Some((_, "msg", _)) => {
69 }
71 _ => bail!(
72 "contract {}: [contract].idl must be a lib-relative file path ending in .srv or .msg, got {idl:?}",
73 c.contract.id
74 ),
75 }
76 }
77 Ok(set)
78}
79
80pub fn generate(
81 resolver: &mut MsgResolver,
82 contracts_dirs: &[PathBuf],
83 out_dir: &Path,
84 verbose: bool,
85) -> Result<()> {
86 let mut paths: Vec<PathBuf> = Vec::new();
87 for d in contracts_dirs {
88 for p in collect_tomls(d)? {
89 paths.push(p);
90 }
91 }
92 if paths.is_empty() {
93 if verbose {
94 for d in contracts_dirs {
95 eprintln!(
96 "[robonix-codegen] contracts: no .toml under {}",
97 d.display()
98 );
99 }
100 }
101 return Ok(());
102 }
103
104 let mut by_id: std::collections::BTreeMap<String, (PathBuf, ContractToml)> =
108 std::collections::BTreeMap::new();
109 for p in paths {
110 let raw =
111 fs::read_to_string(&p).with_context(|| format!("read contract {}", p.display()))?;
112 let c: ContractToml =
113 toml::from_str(&raw).with_context(|| format!("parse TOML {}", p.display()))?;
114 by_id.insert(c.contract.id.clone(), (p, c));
115 }
116 let mut contracts: Vec<(PathBuf, ContractToml)> = by_id.into_values().collect();
117 contracts.sort_by(|a, b| a.1.contract.id.cmp(&b.1.contract.id));
118
119 let mut out = String::new();
120 writeln!(&mut out, "// @generated by robonix-codegen (--contracts).")?;
121 writeln!(&mut out, "// Do not edit by hand.")?;
122 writeln!(&mut out, "syntax = \"proto3\";")?;
123 writeln!(&mut out)?;
124 writeln!(&mut out, "package robonix.contracts;")?;
125 writeln!(&mut out)?;
126 writeln!(&mut out, "import \"google/protobuf/empty.proto\";")?;
127 writeln!(&mut out)?;
128
129 let mut imports: BTreeSet<String> = BTreeSet::new();
130 let mut needs_string_wire = false;
131
132 let mut proto_types: Vec<(String, ResolvedType, ResolvedType)> = Vec::new();
133 for (_, c) in &contracts {
134 let (in_t, out_t) = resolve_contract_io(c, resolver, &mut imports, &mut needs_string_wire)?;
135 proto_types.push((c.contract.id.clone(), in_t, out_t));
136 }
137
138 for imp in &imports {
139 writeln!(&mut out, "import \"{imp}\";",)?;
140 }
141 if !imports.is_empty() {
142 writeln!(&mut out)?;
143 }
144
145 if needs_string_wire {
146 writeln!(
147 &mut out,
148 "// Wrapper for contracts that use primitive/string until shared IDL exists."
149 )?;
150 writeln!(&mut out, "message StringWire {{")?;
151 writeln!(&mut out, " string value = 1;")?;
152 writeln!(&mut out, "}}")?;
153 writeln!(&mut out)?;
154 }
155
156 for ((_, c), (_, in_t, out_t)) in contracts.iter().zip(proto_types.iter()) {
157 let mode = c.mode.mode_type.trim();
158 let svc = contract_id_to_service_name(&c.contract.id);
159 let idl_kind = parse_idl_path(c.contract.idl.trim()).map(|(_, kind, _)| kind);
169 let method_raw = if idl_kind == Some("srv") {
170 parse_idl_path(c.contract.idl.trim())
171 .map(|(_, _, name)| name.to_string())
172 .unwrap_or_else(|| c.contract.id.clone())
173 } else {
174 c.contract
175 .id
176 .rsplit_once('/')
177 .map(|(_, leaf)| leaf.to_string())
178 .unwrap_or_else(|| c.contract.id.clone())
179 };
180 let method = upper_camel(&method_raw);
185 writeln!(
186 &mut out,
187 "// contract: {} (v{})",
188 c.contract.id, c.contract.version
189 )?;
190 writeln!(&mut out, "service {svc} {{")?;
191
192 let rpc = match mode {
193 "rpc" => format_unary(&method, in_t, out_t),
194 "rpc_server_stream" | "topic_out" => format_stream_out(&method, in_t, out_t),
195 "rpc_client_stream" | "topic_in" => format_stream_in(&method, in_t, out_t),
196 "rpc_bidirectional_stream" => format_bidi_stream(&method, in_t, out_t),
197 other => bail!(
198 "unknown [mode].type '{other}' in contract {} (expected rpc | rpc_server_stream | rpc_client_stream | topic_out | topic_in)",
199 c.contract.id
200 ),
201 };
202 writeln!(&mut out, " {rpc}")?;
203
204 writeln!(&mut out, "}}")?;
205 writeln!(&mut out)?;
206 }
207
208 let outfile = out_dir.join("robonix_contracts.proto");
209 fs::write(&outfile, &out).with_context(|| format!("write {}", outfile.display()))?;
210 if verbose {
211 eprintln!(
212 "[robonix-codegen] contracts: wrote {} ({} services)",
213 outfile.display(),
214 contracts.len()
215 );
216 }
217
218 super::contract_proto_modules_gen::write(out_dir, verbose)?;
219 Ok(())
220}
221
222#[derive(Clone)]
223enum ResolvedType {
224 ProtoFqn(String),
225 GoogleEmpty,
226 #[allow(dead_code)]
230 StringWire,
231}
232
233fn format_stream_out(method: &str, input: &ResolvedType, output: &ResolvedType) -> String {
234 format!(
235 "rpc {method}({}) returns (stream {});",
236 empty_or_type(input),
237 stream_element(output)
238 )
239}
240
241fn format_stream_in(method: &str, input: &ResolvedType, output: &ResolvedType) -> String {
242 format!(
243 "rpc {method}(stream {}) returns ({});",
244 stream_element(input),
245 unary_return(output)
246 )
247}
248
249fn format_bidi_stream(method: &str, input: &ResolvedType, output: &ResolvedType) -> String {
250 format!(
251 "rpc {method}(stream {}) returns (stream {});",
252 stream_element(input),
253 stream_element(output)
254 )
255}
256
257fn format_unary(method: &str, input: &ResolvedType, output: &ResolvedType) -> String {
258 format!(
259 "rpc {method}({}) returns ({});",
260 unary_arg(input),
261 unary_return(output)
262 )
263}
264
265fn empty_or_type(t: &ResolvedType) -> String {
266 match t {
267 ResolvedType::GoogleEmpty => "google.protobuf.Empty".to_string(),
268 ResolvedType::ProtoFqn(s) => s.clone(),
269 ResolvedType::StringWire => "robonix.contracts.StringWire".to_string(),
270 }
271}
272
273fn unary_arg(t: &ResolvedType) -> String {
274 empty_or_type(t)
275}
276
277fn unary_return(t: &ResolvedType) -> String {
278 match t {
279 ResolvedType::GoogleEmpty => "google.protobuf.Empty".to_string(),
280 ResolvedType::ProtoFqn(s) => s.clone(),
281 ResolvedType::StringWire => "robonix.contracts.StringWire".to_string(),
282 }
283}
284
285fn stream_element(t: &ResolvedType) -> String {
286 unary_arg(t)
287}
288
289fn srv_stream_field_to_resolved(
290 contract_id: &str,
291 srv_path: &str,
292 section: &str,
293 field: &MsgField,
294 resolver: &mut MsgResolver,
295 imports: &mut BTreeSet<String>,
296 needs_string_wire: &mut bool,
297) -> Result<ResolvedType> {
298 if field.is_array {
299 bail!(
300 "contract {contract_id}: [{section}] stream element must be a single message, not an array (in {srv_path})"
301 );
302 }
303 field_to_resolved_type(field, resolver, imports, needs_string_wire)
304}
305
306fn resolve_contract_io(
307 c: &ContractToml,
308 resolver: &mut MsgResolver,
309 imports: &mut BTreeSet<String>,
310 needs_string_wire: &mut bool,
311) -> Result<(ResolvedType, ResolvedType)> {
312 let mode = c.mode.mode_type.trim();
313 let idl_path = c.contract.idl.trim();
314 let (_, kind, _) = parse_idl_path(idl_path).ok_or_else(|| {
315 anyhow::anyhow!(
316 "contract {}: [contract].idl must be a lib-relative file path ending in .srv or .msg, got {idl_path:?}",
317 c.contract.id
318 )
319 })?;
320
321 if !idl_path_exists(idl_path, resolver) {
326 bail!(
327 "contract {}: idl path {idl_path:?} doesn't resolve to a file under any lib root ({})",
328 c.contract.id,
329 resolver.include_paths.len()
330 );
331 }
332
333 let idl = IdlRef { path: idl_path };
334
335 match (mode, kind) {
336 ("rpc", "srv") => resolve_srv_contract_pair(idl.path, resolver, imports, needs_string_wire),
337 ("rpc_server_stream", "srv") => {
338 resolve_srv_server_stream(&idl, &c.contract.id, resolver, imports, needs_string_wire)
339 }
340 ("rpc_client_stream", "srv") => {
341 resolve_srv_client_stream(&idl, &c.contract.id, resolver, imports, needs_string_wire)
342 }
343 ("rpc_bidirectional_stream", "srv") => {
344 resolve_srv_bidi_stream(&idl, &c.contract.id, resolver, imports, needs_string_wire)
345 }
346 ("topic_out", "msg") => {
347 let elem = resolve_io(idl.path, resolver, imports, needs_string_wire)?;
348 Ok((ResolvedType::GoogleEmpty, elem))
349 }
350 ("topic_in", "msg") => {
351 let elem = resolve_io(idl.path, resolver, imports, needs_string_wire)?;
352 Ok((elem, ResolvedType::GoogleEmpty))
353 }
354 ("rpc" | "rpc_server_stream" | "rpc_client_stream" | "rpc_bidirectional_stream", "msg") => {
355 bail!(
356 "contract {}: mode={mode:?} requires a `.srv` IDL but [contract].idl points at a `.msg` ({idl_path:?})",
357 c.contract.id
358 )
359 }
360 ("topic_out" | "topic_in", "srv") => {
361 bail!(
362 "contract {}: mode={mode:?} requires a `.msg` IDL but [contract].idl points at a `.srv` ({idl_path:?})",
363 c.contract.id
364 )
365 }
366 (other, _) => bail!(
367 "unknown [mode].type {other:?} in contract {}",
368 c.contract.id
369 ),
370 }
371}
372
373fn parse_idl_path(
387 s: &str,
388) -> Option<(
389 &str, &'static str, &str, )> {
393 let (stem, kind): (&str, &'static str) = if let Some(rest) = s.strip_suffix(".srv") {
394 (rest, "srv")
395 } else if let Some(rest) = s.strip_suffix(".msg") {
396 (rest, "msg")
397 } else {
398 return None;
399 };
400 let parts: Vec<&str> = stem.split('/').filter(|p| !p.is_empty()).collect();
401 if parts.is_empty() {
402 return None;
403 }
404 let n = parts.len();
405 let name = parts[n - 1];
406 let pkg = if n >= 3 && (parts[n - 2] == "srv" || parts[n - 2] == "msg") {
407 parts[n - 3]
408 } else {
409 ""
410 };
411 Some((pkg, kind, name))
412}
413
414fn idl_path_exists(idl: &str, resolver: &MsgResolver) -> bool {
418 for root in &resolver.include_paths {
419 if root.join(idl).is_file() {
420 return true;
421 }
422 }
423 false
424}
425
426fn resolve_srv_server_stream(
427 idl: &IdlRef,
428 contract_id: &str,
429 resolver: &mut MsgResolver,
430 imports: &mut BTreeSet<String>,
431 needs_string_wire: &mut bool,
432) -> Result<(ResolvedType, ResolvedType)> {
433 let p = idl.path;
434 let Some((pkg, "srv", name)) = parse_idl_path(p) else {
435 bail!("[contract].idl must end with /srv/Name for rpc modes, got {p:?}");
436 };
437 resolver
438 .resolve_srv(pkg, name)
439 .with_context(|| format!("resolve srv {p}"))?;
440 let spec = resolver
441 .srv_spec(pkg, name)
442 .ok_or_else(|| anyhow::anyhow!("internal: srv {p} not cached"))?
443 .clone();
444
445 let res = &spec.response;
446 if res.fields.len() != 1 {
447 bail!(
448 "contract {contract_id}: [mode] rpc_server_stream requires the .srv response section to have exactly one field (stream element type), got {} in {p}",
449 res.fields.len()
450 );
451 }
452 let in_t = srv_request_to_contract_input(&spec, resolver, imports, needs_string_wire)?;
453 let out_t = srv_stream_field_to_resolved(
454 contract_id,
455 p,
456 "response",
457 &res.fields[0],
458 resolver,
459 imports,
460 needs_string_wire,
461 )?;
462 Ok((in_t, out_t))
463}
464
465fn resolve_srv_client_stream(
466 idl: &IdlRef,
467 contract_id: &str,
468 resolver: &mut MsgResolver,
469 imports: &mut BTreeSet<String>,
470 needs_string_wire: &mut bool,
471) -> Result<(ResolvedType, ResolvedType)> {
472 let p = idl.path;
473 let Some((pkg, "srv", name)) = parse_idl_path(p) else {
474 bail!("[contract].idl must end with /srv/Name for rpc modes, got {p:?}");
475 };
476 resolver
477 .resolve_srv(pkg, name)
478 .with_context(|| format!("resolve srv {p}"))?;
479 let spec = resolver
480 .srv_spec(pkg, name)
481 .ok_or_else(|| anyhow::anyhow!("internal: srv {p} not cached"))?
482 .clone();
483
484 let req = &spec.request;
485 if req.fields.len() != 1 {
486 bail!(
487 "contract {contract_id}: [mode] rpc_client_stream requires the .srv request section to have exactly one field (stream element type), got {} in {p}",
488 req.fields.len()
489 );
490 }
491 let in_t = srv_stream_field_to_resolved(
492 contract_id,
493 p,
494 "request",
495 &req.fields[0],
496 resolver,
497 imports,
498 needs_string_wire,
499 )?;
500 let out_t = srv_response_to_contract_output(&spec, resolver, imports, needs_string_wire)?;
501 Ok((in_t, out_t))
502}
503
504fn resolve_srv_bidi_stream(
509 idl: &IdlRef,
510 contract_id: &str,
511 resolver: &mut MsgResolver,
512 imports: &mut BTreeSet<String>,
513 needs_string_wire: &mut bool,
514) -> Result<(ResolvedType, ResolvedType)> {
515 let p = idl.path;
516 let Some((pkg, "srv", name)) = parse_idl_path(p) else {
517 bail!("[contract].idl must end with /srv/Name for rpc modes, got {p:?}");
518 };
519 resolver
520 .resolve_srv(pkg, name)
521 .with_context(|| format!("resolve srv {p}"))?;
522 let spec = resolver
523 .srv_spec(pkg, name)
524 .ok_or_else(|| anyhow::anyhow!("internal: srv {p} not cached"))?
525 .clone();
526
527 let req = &spec.request;
528 let res = &spec.response;
529 if req.fields.len() != 1 {
530 bail!(
531 "contract {contract_id}: [mode] rpc_bidirectional_stream requires the .srv request section to have exactly one field (client→server stream element type), got {} in {p}",
532 req.fields.len()
533 );
534 }
535 if res.fields.len() != 1 {
536 bail!(
537 "contract {contract_id}: [mode] rpc_bidirectional_stream requires the .srv response section to have exactly one field (server→client stream element type), got {} in {p}",
538 res.fields.len()
539 );
540 }
541 let in_t = srv_stream_field_to_resolved(
542 contract_id,
543 p,
544 "request",
545 &req.fields[0],
546 resolver,
547 imports,
548 needs_string_wire,
549 )?;
550 let out_t = srv_stream_field_to_resolved(
551 contract_id,
552 p,
553 "response",
554 &res.fields[0],
555 resolver,
556 imports,
557 needs_string_wire,
558 )?;
559 Ok((in_t, out_t))
560}
561
562fn srv_request_to_contract_input(
563 srv: &SrvSpec,
564 resolver: &mut MsgResolver,
565 imports: &mut BTreeSet<String>,
566 needs_string_wire: &mut bool,
567) -> Result<ResolvedType> {
568 let req = &srv.request;
569 if req.fields.len() == 1 {
570 return field_to_resolved_type(&req.fields[0], resolver, imports, needs_string_wire);
571 }
572 imports.insert(format!("{}.proto", srv.package));
573 Ok(ResolvedType::ProtoFqn(format!(
574 "{}.{}",
575 proto_package_name(&srv.package),
576 req.name
577 )))
578}
579
580fn srv_response_to_contract_output(
582 srv: &SrvSpec,
583 resolver: &mut MsgResolver,
584 imports: &mut BTreeSet<String>,
585 _needs_string_wire: &mut bool,
586) -> Result<ResolvedType> {
587 let res = &srv.response;
588 if res.fields.is_empty() {
589 return Ok(ResolvedType::GoogleEmpty);
590 }
591 for f in &res.fields {
592 if let MsgTypeRef::Named { package, name } = &f.type_ref {
593 resolver.resolve_named_type(package, name, None)?;
594 }
595 }
596 imports.insert(format!("{}.proto", srv.package));
597 Ok(ResolvedType::ProtoFqn(format!(
598 "{}.{}",
599 proto_package_name(&srv.package),
600 res.name
601 )))
602}
603
604fn resolve_srv_contract_pair(
605 path: &str,
606 resolver: &mut MsgResolver,
607 imports: &mut BTreeSet<String>,
608 _needs_string_wire: &mut bool,
609) -> Result<(ResolvedType, ResolvedType)> {
610 let p = path.trim();
611 if let Some((pkg, "srv", name)) = parse_idl_path(p) {
612 resolver
613 .resolve_srv(pkg, name)
614 .with_context(|| format!("resolve srv {p}"))?;
615 imports.insert(format!("{pkg}.proto"));
616 let req = format!("{name}_Request");
617 let res = format!("{name}_Response");
618 return Ok((
619 ResolvedType::ProtoFqn(format!("{}.{}", proto_package_name(pkg), req)),
620 ResolvedType::ProtoFqn(format!("{}.{}", proto_package_name(pkg), res)),
621 ));
622 }
623 bail!("[contract].idl must end with /srv/Name for rpc modes, got {p:?}");
624}
625
626fn field_to_resolved_type(
627 field: &MsgField,
628 resolver: &mut MsgResolver,
629 imports: &mut BTreeSet<String>,
630 needs_string_wire: &mut bool,
631) -> Result<ResolvedType> {
632 match &field.type_ref {
633 MsgTypeRef::Primitive(_) => bail!(
634 "contract I/O field `{}` must use a named ROS message type, not a primitive",
635 field.name
636 ),
637 MsgTypeRef::Named { package, name } => resolve_io(
638 &format!("{package}/msg/{name}"),
639 resolver,
640 imports,
641 needs_string_wire,
642 ),
643 }
644}
645
646fn resolve_io(
651 spec: &str,
652 resolver: &mut MsgResolver,
653 imports: &mut BTreeSet<String>,
654 _needs_string_wire: &mut bool,
655) -> Result<ResolvedType> {
656 let s = spec.trim();
657 if (s.ends_with(".srv") || s.ends_with(".msg"))
661 && let Some((pkg, kind, name)) = parse_idl_path(s)
662 {
663 return match kind {
664 "msg" => {
665 resolver
666 .resolve_named_type(pkg, name, None)
667 .with_context(|| {
668 format!("resolve msg {pkg}/{name} referenced from contract")
669 })?;
670 imports.insert(format!("{pkg}.proto"));
671 Ok(ResolvedType::ProtoFqn(format!(
672 "{}.{}",
673 proto_package_name(pkg),
674 name
675 )))
676 }
677 "srv" => {
678 resolver.resolve_srv(pkg, name).with_context(|| {
679 format!("resolve srv {pkg}/{name} referenced from contract")
680 })?;
681 imports.insert(format!("{pkg}.proto"));
682 let req = format!("{}_Request", name);
683 Ok(ResolvedType::ProtoFqn(format!(
684 "{}.{}",
685 proto_package_name(pkg),
686 req
687 )))
688 }
689 _ => unreachable!(),
690 };
691 }
692 let parts: Vec<&str> = s.split('/').collect();
695 match parts.as_slice() {
696 [pkg, "msg", name] => {
697 resolver
698 .resolve_named_type(pkg, name, None)
699 .with_context(|| format!("resolve msg {pkg}/{name} referenced from contract"))?;
700 imports.insert(format!("{pkg}.proto"));
701 Ok(ResolvedType::ProtoFqn(format!(
702 "{}.{}",
703 proto_package_name(pkg),
704 name
705 )))
706 }
707 [pkg, "srv", name] => {
708 resolver
709 .resolve_srv(pkg, name)
710 .with_context(|| format!("resolve srv {pkg}/{name} referenced from contract"))?;
711 imports.insert(format!("{pkg}.proto"));
712 let req = format!("{}_Request", name);
713 Ok(ResolvedType::ProtoFqn(format!(
714 "{}.{}",
715 proto_package_name(pkg),
716 req
717 )))
718 }
719 _ => bail!(
720 "unsupported IDL reference {s:?} (expected `<pkg>/msg/<Name>` or `<pkg>/srv/<Name>` for nested refs, or a lib-relative path ending in .srv/.msg for top-level idl)"
721 ),
722 }
723}
724
725#[allow(dead_code)]
726fn parse_ros_path(s: &str) -> Option<(&str, &str, &str)> {
727 let parts: Vec<&str> = s.split('/').collect();
728 if parts.len() != 3 {
729 return None;
730 }
731 Some((parts[0], parts[1], parts[2]))
732}
733
734fn upper_camel(s: &str) -> String {
738 let mut out = String::with_capacity(s.len());
739 let mut capitalize_next = true;
740 for ch in s.chars() {
741 if ch == '_' || ch == '-' {
742 capitalize_next = true;
743 continue;
744 }
745 if capitalize_next {
746 out.extend(ch.to_uppercase());
747 capitalize_next = false;
748 } else {
749 out.push(ch);
750 }
751 }
752 out
753}
754
755fn contract_id_to_service_name(id: &str) -> String {
759 id.split('/')
760 .filter(|x| !x.is_empty())
761 .map(|seg| {
762 seg.split('_')
763 .filter(|p| !p.is_empty())
764 .map(|p| {
765 let mut c = p.chars();
766 match c.next() {
767 None => String::new(),
768 Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
769 }
770 })
771 .collect::<String>()
772 })
773 .collect::<String>()
774}
775
776fn collect_tomls(dir: &Path) -> Result<Vec<PathBuf>> {
777 let mut v = Vec::new();
778 collect_tomls_inner(dir, &mut v)?;
779 v.sort();
780 Ok(v)
781}
782
783fn collect_tomls_inner(dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
784 if !dir.is_dir() {
785 bail!("contracts directory does not exist: {}", dir.display());
786 }
787 for entry in fs::read_dir(dir).with_context(|| format!("read_dir {}", dir.display()))? {
788 let entry = entry?;
789 let p = entry.path();
790 if p.is_dir() {
791 if p.file_name().and_then(|s| s.to_str()) == Some("lib") {
796 continue;
797 }
798 collect_tomls_inner(&p, out)?;
799 } else if p.extension().and_then(|x| x.to_str()) == Some("toml") {
800 out.push(p);
801 }
802 }
803 Ok(())
804}