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