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