1use std::collections::{BTreeMap, HashMap};
2use std::fs;
3use std::path::{Component, Path, PathBuf};
4
5use crate::discovery::read_source;
6use crate::json::json_string;
7
8pub fn is_unpinned_range(range: &str) -> bool {
13 let r = range.trim();
14 if r.is_empty() || r == "*" || r.eq_ignore_ascii_case("x") || r.eq_ignore_ascii_case("latest") {
15 return true;
16 }
17 !r.chars().any(|c| c.is_ascii_digit())
18}
19
20pub fn render_package_json(deps: &std::collections::BTreeMap<String, String>) -> String {
29 let mut out = String::from("{\n \"type\": \"module\"");
30 if !deps.is_empty() {
31 out.push_str(",\n \"dependencies\": {\n");
32 let entries: Vec<String> = deps
33 .iter()
34 .map(|(pkg, range)| format!(" {}: {}", json_string(pkg), json_string(range)))
35 .collect();
36 out.push_str(&entries.join(",\n"));
37 out.push_str("\n }");
38 }
39 out.push_str("\n}\n");
40 out
41}
42
43pub fn normalize_rel(p: &Path) -> PathBuf {
47 let mut out: Vec<std::ffi::OsString> = Vec::new();
48 for c in p.components() {
49 match c {
50 Component::CurDir => {}
51 Component::ParentDir => {
52 out.pop();
53 }
54 Component::Normal(s) => out.push(s.to_os_string()),
55 Component::RootDir | Component::Prefix(_) => {}
56 }
57 }
58 out.iter().collect()
59}
60
61#[derive(Debug, Clone)]
68pub struct ProjectPaths {
69 pub include: Vec<PathBuf>,
73 pub exclude: Vec<PathBuf>,
76}
77
78impl ProjectPaths {
79 pub fn conventional(project_root: &Path) -> Self {
85 let mut include = Vec::new();
86 for role in ["src", "tests"] {
87 if project_root.join(role).is_dir() {
88 include.push(PathBuf::from(role));
89 }
90 }
91 if include.is_empty() {
92 include.push(PathBuf::from("."));
93 }
94 ProjectPaths {
95 include,
96 exclude: Vec::new(),
97 }
98 }
99}
100
101#[cfg(test)]
110pub(crate) fn read_project_paths_with(
111 project_root: &Path,
112 overlay: &HashMap<PathBuf, String>,
113) -> ProjectPaths {
114 try_read_project_paths_with(project_root, overlay)
115 .unwrap_or_else(|_| ProjectPaths::conventional(project_root))
116}
117
118#[derive(Debug)]
121pub enum ProjectPathsError {
122 Malformed,
124 UnknownKey(String),
127 UnknownTable(String),
131 TopLevelKey(String),
133 UnknownTableKey { table: &'static str, key: String },
137 NotAStringList {
141 table: &'static str,
142 key: &'static str,
143 },
144 NotABool {
147 table: &'static str,
148 key: &'static str,
149 },
150 BadTestTarget { target: String, reason: String },
152 ProjectAndPackage,
155 BadPackage { key: &'static str, reason: String },
157 BadDependency {
160 table: &'static str,
161 alias: String,
162 reason: String,
163 },
164 UnknownEntryKey {
166 table: &'static str,
167 entry: String,
168 key: String,
169 },
170 BadRegistry { name: String, reason: String },
172 RegistryCredential { name: String },
174 BadWorkspace { reason: String },
177 WorkspaceRootTable(String),
180 WorkspaceRootSources,
182 WorkspaceRoot,
185 MemberRegistries { workspace: PathBuf },
188 NestedWorkspace { workspace: PathBuf },
190}
191
192pub const MANIFEST_TABLES: &[(&str, &[&str])] = &[
196 ("project", &["name", "version"]),
197 (
198 "package",
199 &[
200 "name",
201 "version",
202 "description",
203 "license",
204 "repository",
205 "exports",
206 ],
207 ),
208 ("paths", &["include", "exclude"]),
209 ("fmt", &[]),
210 ("lsp", &["diagnostics_mode", "diagnostics_debounce_ms"]),
211 ("workers", &["compatibility_flags", "workers_dev"]),
212 ("unstable", &["allow"]),
213 ("test", &["targets"]),
214 ("workspace", &["members", "exclude"]),
215];
216
217pub const MANIFEST_MAP_TABLES: &[(&str, &[&str])] = &[
223 ("dependencies", crate::manifest::DEPENDENCY_KEYS),
224 ("dev-dependencies", crate::manifest::DEPENDENCY_KEYS),
225 ("registries", crate::manifest::REGISTRY_KEYS),
226];
227
228const WORKSPACE_ROOT_TABLES: &[&str] = &["workspace", "registries"];
230
231const PLANNED_TABLES: &[(&str, &str)] = &[("deploy", "#551")];
234
235fn all_tables() -> impl Iterator<Item = &'static str> {
238 MANIFEST_TABLES
239 .iter()
240 .chain(MANIFEST_MAP_TABLES)
241 .map(|(t, _)| *t)
242}
243
244fn table_list(conj: &str) -> String {
248 let names: Vec<String> = all_tables().map(|t| format!("`[{t}]`")).collect();
249 match names.split_last() {
250 Some((last, rest)) if !rest.is_empty() => format!("{} {conj} {last}", rest.join(", ")),
251 _ => names.join(""),
252 }
253}
254
255pub(crate) fn did_you_mean<'a>(
257 name: &str,
258 candidates: impl IntoIterator<Item = &'a str>,
259) -> Option<&'a str> {
260 candidates
261 .into_iter()
262 .map(|c| (edit_distance(name, c), c))
263 .filter(|(d, _)| *d <= 2)
264 .min_by_key(|(d, _)| *d)
265 .map(|(_, c)| c)
266}
267
268fn edit_distance(a: &str, b: &str) -> usize {
270 let b: Vec<char> = b.chars().collect();
271 let mut row: Vec<usize> = (0..=b.len()).collect();
272 for (i, ca) in a.chars().enumerate() {
273 let mut prev = row[0];
274 row[0] = i + 1;
275 for (j, cb) in b.iter().enumerate() {
276 let cur = row[j + 1];
277 row[j + 1] = (prev + usize::from(ca != *cb)).min(row[j] + 1).min(cur + 1);
278 prev = cur;
279 }
280 }
281 row[b.len()]
282}
283
284impl std::fmt::Display for ProjectPathsError {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 match self {
287 ProjectPathsError::Malformed => write!(f, "`bynk.toml` is not valid TOML"),
288 ProjectPathsError::UnknownKey(k) => {
289 write!(
290 f,
291 "`[paths]` has no key named `{k}` — did you mean `include` or `exclude`?"
292 )
293 }
294 ProjectPathsError::UnknownTable(name) => {
295 write!(f, "`bynk.toml` has no table named `[{name}]` — ")?;
296 if let Some((_, issue)) = PLANNED_TABLES.iter().find(|(t, _)| *t == name) {
297 write!(f, "it is planned but not yet supported ({issue})")
298 } else if let Some(t) = did_you_mean(name, all_tables()) {
299 write!(f, "did you mean `[{t}]`?")
300 } else {
301 write!(f, "the tables are {}", table_list("and"))
302 }
303 }
304 ProjectPathsError::TopLevelKey(key) => {
305 if all_tables().any(|t| t == key) {
308 return write!(
309 f,
310 "`{key}` in `bynk.toml` must be a table — write it as `[{key}]`"
311 );
312 }
313 write!(f, "`bynk.toml` has a key `{key}` outside any table — ")?;
314 match MANIFEST_TABLES
315 .iter()
316 .find(|(_, keys)| keys.contains(&key.as_str()))
317 {
318 Some((table, _)) => write!(f, "did you mean it under `[{table}]`?"),
319 None => write!(f, "keys belong in {}", table_list("or")),
320 }
321 }
322 ProjectPathsError::UnknownTableKey { table, key } => {
323 let keys = MANIFEST_TABLES
324 .iter()
325 .find(|(t, _)| t == table)
326 .map(|(_, k)| *k)
327 .unwrap_or_default();
328 write!(f, "`[{table}]` has no key named `{key}` — ")?;
329 match did_you_mean(key, keys.iter().copied()) {
330 Some(k) => write!(f, "did you mean `{k}`?"),
331 None => {
332 let list: Vec<String> = keys.iter().map(|k| format!("`{k}`")).collect();
333 write!(f, "its keys are {}", list.join(", "))
334 }
335 }
336 }
337 ProjectPathsError::NotAStringList { table, key } => write!(
338 f,
339 "`[{table}]` `{key}` must be a list of strings, like `{key} = [\"nodejs_compat\"]`"
340 ),
341 ProjectPathsError::NotABool { table, key } => write!(
342 f,
343 "`[{table}]` `{key}` must be `true` or `false`, like `{key} = false`"
344 ),
345 ProjectPathsError::BadTestTarget { target, reason } => {
346 write!(f, "`[test.targets.{target}]` {reason}")
347 }
348 ProjectPathsError::ProjectAndPackage => write!(
349 f,
350 "`bynk.toml` has both `[project]` and `[package]` — a package is named by `[package] name`, so remove `[project]`"
351 ),
352 ProjectPathsError::BadPackage { key, reason } => {
353 write!(f, "`[package]` `{key}` {reason}")
354 }
355 ProjectPathsError::BadDependency {
356 table,
357 alias,
358 reason,
359 } => write!(f, "`[{table}]` `{alias}` {reason}"),
360 ProjectPathsError::UnknownEntryKey { table, entry, key } => {
361 let keys = MANIFEST_MAP_TABLES
362 .iter()
363 .find(|(t, _)| t == table)
364 .map(|(_, k)| *k)
365 .unwrap_or_default();
366 let place = if *table == "registries" {
367 format!("`[registries.{entry}]`")
368 } else {
369 format!("`[{table}]` `{entry}`")
370 };
371 write!(f, "{place} has no key named `{key}` — ")?;
372 match did_you_mean(key, keys.iter().copied()) {
373 Some(k) => write!(f, "did you mean `{k}`?"),
374 None => {
375 let list: Vec<String> = keys.iter().map(|k| format!("`{k}`")).collect();
376 write!(f, "its keys are {}", list.join(", "))
377 }
378 }
379 }
380 ProjectPathsError::BadRegistry { name, reason } => {
381 write!(f, "`[registries.{name}]` {reason}")
382 }
383 ProjectPathsError::RegistryCredential { name } => write!(
384 f,
385 "`[registries.{name}]` has a `token` — credentials belong in `{}` or the user credentials file, never in `bynk.toml`",
386 crate::manifest::registry_token_var(name)
387 ),
388 ProjectPathsError::BadWorkspace { reason } => write!(f, "`[workspace]` {reason}"),
389 ProjectPathsError::WorkspaceRootTable(table) => write!(
390 f,
391 "a workspace root's `bynk.toml` holds only `[workspace]` and `[registries]` — `[{table}]` belongs in a member's `bynk.toml`"
392 ),
393 ProjectPathsError::WorkspaceRootSources => write!(
394 f,
395 "a workspace root holds no sources of its own — move them into a member"
396 ),
397 ProjectPathsError::WorkspaceRoot => write!(
398 f,
399 "this is a workspace root, not a project — run the command inside a member (`bynk check`, `fmt` and `test` run across every member from here)"
400 ),
401 ProjectPathsError::MemberRegistries { workspace } => write!(
402 f,
403 "`[registries]` belongs in the workspace root's `bynk.toml` (`{}`), not a member's",
404 workspace.join("bynk.toml").display()
405 ),
406 ProjectPathsError::NestedWorkspace { workspace } => write!(
407 f,
408 "this project is a member of the workspace at `{}`, so it can't declare a `[workspace]` of its own",
409 workspace.display()
410 ),
411 }
412 }
413}
414
415pub fn check_manifest(
426 project_root: &Path,
427 overlay: &HashMap<PathBuf, String>,
428) -> Result<(), ProjectPathsError> {
429 let toml_path = project_root.join("bynk.toml");
430 let Ok(content) = read_source(&toml_path, overlay) else {
431 return Ok(());
432 };
433 check_manifest_str(&content)?;
434 let doc = content
438 .parse::<toml::Table>()
439 .map_err(|_| ProjectPathsError::Malformed)?;
440 if (doc.contains_key("registries") || doc.contains_key("workspace"))
441 && let Some(workspace) = crate::workspace::find_parent_workspace(project_root)
442 {
443 return Err(if doc.contains_key("workspace") {
444 ProjectPathsError::NestedWorkspace { workspace }
445 } else {
446 ProjectPathsError::MemberRegistries { workspace }
447 });
448 }
449 Ok(())
450}
451
452pub fn check_manifest_str(content: &str) -> Result<(), ProjectPathsError> {
455 let doc = content
456 .parse::<toml::Table>()
457 .map_err(|_| ProjectPathsError::Malformed)?;
458 let mut declared: Vec<&'static str> = Vec::new();
459 for (name, value) in &doc {
460 if PLANNED_TABLES.iter().any(|(t, _)| t == name) {
463 return Err(ProjectPathsError::UnknownTable(name.clone()));
464 }
465 let known = MANIFEST_TABLES
466 .iter()
467 .map(|(t, keys)| (*t, *keys, false))
468 .chain(
469 MANIFEST_MAP_TABLES
470 .iter()
471 .map(|(t, keys)| (*t, *keys, true)),
472 )
473 .find(|(t, _, _)| t == name);
474 let Some((table, keys, is_map)) = known else {
475 return Err(if value.is_table() {
476 ProjectPathsError::UnknownTable(name.clone())
477 } else {
478 ProjectPathsError::TopLevelKey(name.clone())
479 });
480 };
481 let Some(entries) = value.as_table() else {
485 return Err(ProjectPathsError::TopLevelKey(name.clone()));
486 };
487 declared.push(table);
488 if is_map {
491 match table {
492 "dependencies" | "dev-dependencies" => {
493 crate::manifest::dependencies_from(table, entries)?;
494 }
495 _ => {
496 crate::manifest::registries_from(entries)?;
497 }
498 }
499 continue;
500 }
501 if keys.is_empty() {
502 continue;
503 }
504 for key in entries.keys() {
505 if !keys.contains(&key.as_str()) {
506 return Err(match table {
507 "paths" => ProjectPathsError::UnknownKey(key.clone()),
508 _ => ProjectPathsError::UnknownTableKey {
509 table,
510 key: key.clone(),
511 },
512 });
513 }
514 }
515 match table {
516 "workers" => drop(workers_config_from(entries)?),
517 "unstable" => drop(unstable_config_from(entries)?),
518 "test" => drop(test_targets_from(entries)?),
519 "package" => drop(crate::manifest::package_config_from(entries)?),
520 "workspace" => drop(crate::manifest::workspace_config_from(entries)?),
521 _ => {}
522 }
523 }
524 if declared.contains(&"project") && declared.contains(&"package") {
526 return Err(ProjectPathsError::ProjectAndPackage);
527 }
528 if declared.contains(&"workspace")
529 && let Some(other) = declared.iter().find(|t| !WORKSPACE_ROOT_TABLES.contains(t))
530 {
531 return Err(ProjectPathsError::WorkspaceRootTable((*other).to_string()));
532 }
533 Ok(())
534}
535
536pub fn try_read_project_paths(project_root: &Path) -> Result<ProjectPaths, ProjectPathsError> {
546 let toml_path = project_root.join("bynk.toml");
547 let overlay = match fs::read_to_string(&toml_path) {
548 Ok(text) => HashMap::from([(toml_path, text)]),
549 Err(_) => HashMap::new(),
550 };
551 try_read_project_paths_with(project_root, &overlay)
552}
553
554pub fn try_read_project_paths_with(
557 project_root: &Path,
558 overlay: &HashMap<PathBuf, String>,
559) -> Result<ProjectPaths, ProjectPathsError> {
560 let toml_path = project_root.join("bynk.toml");
561 let Ok(content) = read_source(&toml_path, overlay) else {
562 return Ok(ProjectPaths::conventional(project_root));
563 };
564 let Ok(doc) = content.parse::<toml::Table>() else {
565 return Err(ProjectPathsError::Malformed);
566 };
567 let paths = doc.get("paths").and_then(|v| v.as_table());
568 if let Some(t) = paths {
569 for k in t.keys() {
570 if k != "include" && k != "exclude" {
571 return Err(ProjectPathsError::UnknownKey(k.clone()));
572 }
573 }
574 }
575 let list = |key: &str| -> Vec<PathBuf> {
576 match paths.and_then(|t| t.get(key)) {
577 Some(toml::Value::Array(items)) => items
578 .iter()
579 .filter_map(|v| v.as_str())
580 .map(PathBuf::from)
581 .collect(),
582 Some(toml::Value::String(s)) => vec![PathBuf::from(s)],
583 _ => Vec::new(),
584 }
585 };
586 let mut include = list("include");
587 let exclude = list("exclude");
588 if include.is_empty() {
589 include = ProjectPaths::conventional(project_root).include;
590 }
591 Ok(ProjectPaths { include, exclude })
592}
593
594#[derive(Debug, Clone, Default, PartialEq, Eq)]
597pub struct WorkersConfig {
598 pub compatibility_flags: Vec<String>,
604 pub workers_dev: Option<bool>,
608}
609
610pub fn try_read_workers_config_with(
618 project_root: &Path,
619 overlay: &HashMap<PathBuf, String>,
620) -> Result<WorkersConfig, ProjectPathsError> {
621 let toml_path = project_root.join("bynk.toml");
622 let Ok(content) = read_source(&toml_path, overlay) else {
623 return Ok(WorkersConfig::default());
624 };
625 let doc = content
626 .parse::<toml::Table>()
627 .map_err(|_| ProjectPathsError::Malformed)?;
628 match doc.get("workers").and_then(|v| v.as_table()) {
629 Some(t) => workers_config_from(t),
630 None => Ok(WorkersConfig::default()),
631 }
632}
633
634pub fn try_read_workers_config(project_root: &Path) -> Result<WorkersConfig, ProjectPathsError> {
636 let toml_path = project_root.join("bynk.toml");
637 let overlay = match fs::read_to_string(&toml_path) {
638 Ok(text) => HashMap::from([(toml_path, text)]),
639 Err(_) => HashMap::new(),
640 };
641 try_read_workers_config_with(project_root, &overlay)
642}
643
644#[derive(Debug, Clone, Default, PartialEq, Eq)]
647pub struct TestTarget {
648 pub urls: BTreeMap<String, String>,
651 pub secrets: Vec<String>,
654 pub production: bool,
656 pub vars: BTreeMap<String, String>,
659}
660
661pub fn try_read_test_targets(
665 project_root: &Path,
666) -> Result<BTreeMap<String, TestTarget>, ProjectPathsError> {
667 let Ok(content) = fs::read_to_string(project_root.join("bynk.toml")) else {
668 return Ok(BTreeMap::new());
669 };
670 let doc = content
671 .parse::<toml::Table>()
672 .map_err(|_| ProjectPathsError::Malformed)?;
673 match doc.get("test").and_then(|v| v.as_table()) {
674 Some(t) => test_targets_from(t),
675 None => Ok(BTreeMap::new()),
676 }
677}
678
679fn test_targets_from(
683 table: &toml::Table,
684) -> Result<BTreeMap<String, TestTarget>, ProjectPathsError> {
685 let mut out = BTreeMap::new();
686 let Some(targets) = table.get("targets") else {
687 return Ok(out);
688 };
689 let Some(targets) = targets.as_table() else {
690 return Err(ProjectPathsError::BadTestTarget {
691 target: "…".to_string(),
692 reason: "must be a table of named targets, like `[test.targets.preview]`".to_string(),
693 });
694 };
695 for (name, entry) in targets {
696 let bad = |reason: &str| ProjectPathsError::BadTestTarget {
697 target: name.clone(),
698 reason: reason.to_string(),
699 };
700 let Some(entry) = entry.as_table() else {
701 return Err(bad("must be a table"));
702 };
703 let strings =
704 |v: &toml::Value, what: &str| -> Result<BTreeMap<String, String>, ProjectPathsError> {
705 v.as_table()
706 .ok_or_else(|| bad(&format!("`{what}` must be a table of strings")))?
707 .iter()
708 .map(|(k, v)| {
709 v.as_str()
710 .map(|s| (k.clone(), s.to_string()))
711 .ok_or_else(|| bad(&format!("`{what}.{k}` must be a string")))
712 })
713 .collect()
714 };
715 let mut target = TestTarget::default();
716 for (key, value) in entry {
717 match key.as_str() {
718 "url" => target.urls = strings(value, "url")?,
719 "vars" => target.vars = strings(value, "vars")?,
720 "secrets" => {
721 target.secrets = value
722 .as_array()
723 .and_then(|a| a.iter().map(|v| v.as_str().map(str::to_string)).collect())
724 .ok_or_else(|| bad("`secrets` must be a list of names"))?;
725 }
726 "production" => {
727 target.production = value
728 .as_bool()
729 .ok_or_else(|| bad("`production` must be `true` or `false`"))?;
730 }
731 other => {
732 return Err(bad(&format!(
733 "has no key named `{other}` — its keys are `url`, `secrets`, `vars` and `production`"
734 )));
735 }
736 }
737 }
738 if target.urls.is_empty() {
739 return Err(bad(
740 "names no Worker URL — add `url.<worker> = \"https://…\"`",
741 ));
742 }
743 out.insert(name.clone(), target);
744 }
745 Ok(out)
746}
747
748pub fn duplicate_compatibility_flag_warning(
756 flag: &str,
757 default: bool,
758 manifest: PathBuf,
759) -> crate::AttributedError {
760 let message = if default {
761 format!(
762 "`bynk.toml`'s `[workers] compatibility_flags` lists `{flag}`, which every Bynk Worker already has"
763 )
764 } else {
765 format!("`bynk.toml`'s `[workers] compatibility_flags` lists `{flag}` more than once")
766 };
767 let mut error = bynk_syntax::CompileError::new(
768 "bynk.project.duplicate_compatibility_flag",
769 bynk_syntax::span::Span::default(),
770 message,
771 );
772 error
773 .notes
774 .push("the duplicate is dropped; remove it from `bynk.toml`".to_string());
775 crate::AttributedError::new(Some(manifest), error)
776}
777
778fn workers_config_from(table: &toml::Table) -> Result<WorkersConfig, ProjectPathsError> {
781 let not_a_list = || ProjectPathsError::NotAStringList {
782 table: "workers",
783 key: "compatibility_flags",
784 };
785 let compatibility_flags = match table.get("compatibility_flags") {
786 None => Vec::new(),
787 Some(toml::Value::Array(items)) => items
788 .iter()
789 .map(|v| v.as_str().map(str::to_owned).ok_or_else(not_a_list))
790 .collect::<Result<_, _>>()?,
791 Some(_) => return Err(not_a_list()),
792 };
793 let workers_dev = match table.get("workers_dev") {
794 None => None,
795 Some(toml::Value::Boolean(b)) => Some(*b),
796 Some(_) => {
797 return Err(ProjectPathsError::NotABool {
798 table: "workers",
799 key: "workers_dev",
800 });
801 }
802 };
803 Ok(WorkersConfig {
804 compatibility_flags,
805 workers_dev,
806 })
807}
808
809#[derive(Debug, Clone, Default, PartialEq, Eq)]
812pub struct UnstableConfig {
813 pub allow: Vec<String>,
818}
819
820pub fn try_read_unstable_config_with(
825 project_root: &Path,
826 overlay: &HashMap<PathBuf, String>,
827) -> Result<UnstableConfig, ProjectPathsError> {
828 let toml_path = project_root.join("bynk.toml");
829 let Ok(content) = read_source(&toml_path, overlay) else {
830 return Ok(UnstableConfig::default());
831 };
832 let doc = content
833 .parse::<toml::Table>()
834 .map_err(|_| ProjectPathsError::Malformed)?;
835 match doc.get("unstable").and_then(|v| v.as_table()) {
836 Some(t) => unstable_config_from(t),
837 None => Ok(UnstableConfig::default()),
838 }
839}
840
841pub fn try_read_unstable_config(project_root: &Path) -> Result<UnstableConfig, ProjectPathsError> {
843 let toml_path = project_root.join("bynk.toml");
844 let overlay = match fs::read_to_string(&toml_path) {
845 Ok(text) => HashMap::from([(toml_path, text)]),
846 Err(_) => HashMap::new(),
847 };
848 try_read_unstable_config_with(project_root, &overlay)
849}
850
851pub fn read_unstable_config(
857 project_root: &Path,
858 overlay: &HashMap<PathBuf, String>,
859) -> UnstableConfig {
860 let manifest = project_root.join("bynk.toml");
861 let read = if overlay.contains_key(&manifest) {
862 try_read_unstable_config_with(project_root, overlay)
863 } else {
864 try_read_unstable_config(project_root)
865 };
866 read.unwrap_or_default()
867}
868
869fn unstable_config_from(table: &toml::Table) -> Result<UnstableConfig, ProjectPathsError> {
872 let not_a_list = || ProjectPathsError::NotAStringList {
873 table: "unstable",
874 key: "allow",
875 };
876 let allow = match table.get("allow") {
877 None => Vec::new(),
878 Some(toml::Value::Array(items)) => items
879 .iter()
880 .map(|v| v.as_str().map(str::to_owned).ok_or_else(not_a_list))
881 .collect::<Result<_, _>>()?,
882 Some(_) => return Err(not_a_list()),
883 };
884 Ok(UnstableConfig { allow })
885}
886
887pub fn commons_dir_for(name: &str) -> PathBuf {
893 let mut p = PathBuf::new();
894 let mut parts = name.split('.');
895 let first = parts.next().unwrap_or_default();
896 match crate::packages::PackageId::decode(first) {
897 Some(id) => p.push(id.out_dir()),
898 None => p.push(first),
899 }
900 for part in parts {
901 p.push(part);
902 }
903 p
904}
905
906pub fn ts_output_path(source: &Path) -> PathBuf {
907 let mut out = source.to_path_buf();
908 out.set_extension("ts");
909 out
910}
911
912pub fn worker_dir_name(context: &str) -> String {
915 context.replace('.', "-")
916}
917
918pub fn worker_handlers_source_path(context: &str) -> PathBuf {
922 PathBuf::from(format!(
923 "workers/{}/handlers.bynk",
924 worker_dir_name(context)
925 ))
926}
927
928pub fn worker_handlers_output_path(context: &str) -> PathBuf {
930 PathBuf::from(format!("workers/{}/handlers.ts", worker_dir_name(context)))
931}
932
933pub fn worker_file_source_path(context: &str, source: &Path) -> PathBuf {
945 let stem = source.file_stem().unwrap_or_default().to_string_lossy();
946 PathBuf::from(format!(
947 "workers/{}/handlers/{stem}.bynk",
948 worker_dir_name(context)
949 ))
950}
951
952fn stem_parts(rel_path: &Path) -> Vec<String> {
955 rel_path
956 .with_extension("")
957 .components()
958 .filter_map(|c| match c {
959 Component::Normal(s) => Some(s.to_string_lossy().to_string()),
960 _ => None,
961 })
962 .collect()
963}
964
965pub fn unit_path_matches(rel_path: &Path, qualified_name: &str) -> bool {
974 let dir = commons_dir_for(qualified_name);
977 let owned: Vec<String> = dir
978 .components()
979 .map(|c| c.as_os_str().to_string_lossy().into_owned())
980 .collect();
981 let name_parts: Vec<&str> = owned.iter().map(String::as_str).collect();
982 let stem_parts = stem_parts(rel_path);
983 let single_file_match = stem_parts.len() == name_parts.len()
984 && stem_parts
985 .iter()
986 .zip(name_parts.iter())
987 .all(|(a, b)| a == b);
988 single_file_match || is_multi_file_parts(&stem_parts, &name_parts)
989}
990
991fn is_multi_file_parts(stem_parts: &[String], name_parts: &[&str]) -> bool {
994 if stem_parts.is_empty() {
995 return false;
996 }
997 let parent_parts = &stem_parts[..stem_parts.len() - 1];
998 parent_parts.len() == name_parts.len()
999 && parent_parts
1000 .iter()
1001 .zip(name_parts.iter())
1002 .all(|(a, b)| a == b)
1003}
1004
1005pub fn is_multi_file_layout(rel_path: &Path, qualified_name: &str) -> bool {
1015 let name_parts: Vec<&str> = qualified_name.split('.').collect();
1016 is_multi_file_parts(&stem_parts(rel_path), &name_parts)
1017}
1018
1019pub fn renamed_unit_name(old_rel: &Path, old_name: &str, new_rel: &Path) -> Option<String> {
1034 let name_parts: Vec<&str> = old_name.split('.').collect();
1035 let old_stem = stem_parts(old_rel);
1036 let new_stem = stem_parts(new_rel);
1037
1038 let suffix_matches = |haystack: &[String]| {
1039 haystack.len() >= name_parts.len() && {
1040 let prefix_len = haystack.len() - name_parts.len();
1041 haystack[prefix_len..]
1042 .iter()
1043 .zip(name_parts.iter())
1044 .all(|(a, b)| a == b)
1045 }
1046 };
1047
1048 if suffix_matches(&old_stem) {
1049 let prefix_len = old_stem.len() - name_parts.len();
1050 return (new_stem.len() >= prefix_len).then(|| new_stem[prefix_len..].join("."));
1051 }
1052 if !old_stem.is_empty() {
1053 let old_parent = &old_stem[..old_stem.len() - 1];
1054 if suffix_matches(old_parent) {
1055 let prefix_len = old_parent.len() - name_parts.len();
1056 if new_stem.is_empty() {
1057 return None;
1058 }
1059 let new_parent = &new_stem[..new_stem.len() - 1];
1060 return (new_parent.len() >= prefix_len).then(|| new_parent[prefix_len..].join("."));
1061 }
1062 }
1063 None
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068 use super::*;
1069 use std::path::{Path, PathBuf};
1070
1071 #[test]
1073 fn is_unpinned_range_true_for_wildcards_and_digitless() {
1074 assert!(is_unpinned_range(""));
1075 assert!(is_unpinned_range("*"));
1076 assert!(is_unpinned_range("x"));
1077 assert!(is_unpinned_range("X"));
1078 assert!(is_unpinned_range("latest"));
1079 assert!(is_unpinned_range("LATEST"));
1080 assert!(is_unpinned_range(" * ")); assert!(is_unpinned_range("workspace:*")); assert!(is_unpinned_range("beta"));
1083 }
1084
1085 #[test]
1086 fn is_unpinned_range_false_when_a_digit_is_present() {
1087 assert!(!is_unpinned_range("1.0.0"));
1088 assert!(!is_unpinned_range("^1.2"));
1089 assert!(!is_unpinned_range("~0.1"));
1090 assert!(!is_unpinned_range(">=2"));
1091 assert!(!is_unpinned_range("18"));
1092 }
1093
1094 #[test]
1096 fn read_project_paths_with_honours_a_virtual_bynk_toml() {
1097 let root = PathBuf::from("/nonexistent-bynk-test-root-57");
1098 let mut overlay = HashMap::new();
1099 overlay.insert(
1100 root.join("bynk.toml"),
1101 "[paths]\ninclude = [\"app\"]\nexclude = [\"vendor\"]\n".to_string(),
1102 );
1103 let paths = read_project_paths_with(&root, &overlay);
1104 assert_eq!(paths.include, vec![PathBuf::from("app")]);
1105 assert_eq!(paths.exclude, vec![PathBuf::from("vendor")]);
1106 }
1107
1108 #[test]
1109 fn read_project_paths_with_falls_back_to_conventional_with_no_overlay_entry() {
1110 let root = PathBuf::from("/nonexistent-bynk-test-root-57-empty");
1113 let paths = read_project_paths_with(&root, &HashMap::new());
1114 let conventional = ProjectPaths::conventional(&root);
1115 assert_eq!(paths.include, conventional.include);
1116 assert_eq!(paths.exclude, conventional.exclude);
1117 }
1118
1119 #[test]
1122 fn read_project_paths_with_honours_three_or_more_include_entries() {
1123 let root = PathBuf::from("/nonexistent-bynk-test-root-1113-many-includes");
1124 let mut overlay = HashMap::new();
1125 overlay.insert(
1126 root.join("bynk.toml"),
1127 "[paths]\ninclude = [\"src\", \"tests\", \"examples\"]\n".to_string(),
1128 );
1129 let paths = try_read_project_paths_with(&root, &overlay).expect("must parse");
1130 assert_eq!(
1131 paths.include,
1132 vec![
1133 PathBuf::from("src"),
1134 PathBuf::from("tests"),
1135 PathBuf::from("examples"),
1136 ]
1137 );
1138 }
1139
1140 #[test]
1142 fn render_package_json_declares_es_modules() {
1143 let bare = render_package_json(&std::collections::BTreeMap::new());
1145 assert_eq!(bare, "{\n \"type\": \"module\"\n}\n");
1146 let mut deps = std::collections::BTreeMap::new();
1147 deps.insert("jose".to_string(), "^5.0.0".to_string());
1148 let with_deps = render_package_json(&deps);
1149 assert_eq!(
1150 with_deps,
1151 "{\n \"type\": \"module\",\n \"dependencies\": {\n \"jose\": \"^5.0.0\"\n }\n}\n"
1152 );
1153 }
1154
1155 #[test]
1156 fn render_package_json_renders_sorted_dependencies() {
1157 let mut deps = std::collections::BTreeMap::new();
1158 deps.insert("zod".to_string(), "^3.22.4".to_string());
1159 deps.insert("hono".to_string(), "^4.0.0".to_string());
1160 let out = render_package_json(&deps);
1161 assert!(
1163 out.find("\"hono\"").unwrap() < out.find("\"zod\"").unwrap(),
1164 "dependencies render in sorted order:\n{out}"
1165 );
1166 assert!(out.contains("\"hono\": \"^4.0.0\""), "{out}");
1167 }
1168
1169 #[test]
1175 fn render_package_json_escapes_the_control_range() {
1176 let mut deps = std::collections::BTreeMap::new();
1177 deps.insert("pkg\nname".to_string(), "^1.0\u{1}0".to_string());
1178 let out = render_package_json(&deps);
1179 assert!(out.contains("\"pkg\\nname\""), "{out}");
1180 assert!(out.contains("\"^1.0\\u00010\""), "{out}");
1181 assert!(
1184 !out.lines().any(|l| l.chars().any(|c| (c as u32) < 0x20)),
1185 "a raw control character reached the output:\n{out:?}"
1186 );
1187 }
1188
1189 #[test]
1190 fn render_package_json_escapes_structural_characters() {
1191 let mut deps = std::collections::BTreeMap::new();
1192 deps.insert("a\"b".to_string(), "c\\d".to_string());
1193 let out = render_package_json(&deps);
1194 assert!(out.contains(r#""a\"b": "c\\d""#), "{out}");
1195 }
1196
1197 #[test]
1199 fn normalize_rel_resolves_dot_and_parent() {
1200 assert_eq!(
1201 normalize_rel(Path::new("./tokens.binding.ts")),
1202 PathBuf::from("tokens.binding.ts")
1203 );
1204 assert_eq!(normalize_rel(Path::new("a/./b")), PathBuf::from("a/b"));
1205 assert_eq!(normalize_rel(Path::new("a/../b")), PathBuf::from("b"));
1206 assert_eq!(normalize_rel(Path::new("a/b/../../c")), PathBuf::from("c"));
1207 assert_eq!(normalize_rel(Path::new("a/b")), PathBuf::from("a/b"));
1208 }
1209
1210 #[test]
1211 fn normalize_rel_drops_root_and_pops_through_empty() {
1212 assert_eq!(normalize_rel(Path::new("/a/b")), PathBuf::from("a/b"));
1214 assert_eq!(normalize_rel(Path::new("../a")), PathBuf::from("a"));
1216 }
1217
1218 #[test]
1220 fn commons_dir_for_splits_dotted_name_into_dirs() {
1221 assert_eq!(commons_dir_for("a.b.c"), PathBuf::from("a/b/c"));
1222 assert_eq!(commons_dir_for("foo"), PathBuf::from("foo"));
1223 }
1224
1225 #[test]
1226 fn ts_output_path_sets_ts_extension() {
1227 assert_eq!(
1228 ts_output_path(Path::new("foo.bynk")),
1229 PathBuf::from("foo.ts")
1230 );
1231 assert_eq!(
1232 ts_output_path(Path::new("a/b.bynk")),
1233 PathBuf::from("a/b.ts")
1234 );
1235 assert_eq!(ts_output_path(Path::new("foo")), PathBuf::from("foo.ts"));
1236 }
1237
1238 #[test]
1240 fn worker_paths_dasherise_and_root_under_workers() {
1241 assert_eq!(worker_dir_name("commerce.payment"), "commerce-payment");
1242 assert_eq!(worker_dir_name("plain"), "plain");
1243 assert_eq!(
1244 worker_handlers_source_path("commerce.payment"),
1245 PathBuf::from("workers/commerce-payment/handlers.bynk")
1246 );
1247 assert_eq!(
1248 worker_handlers_output_path("commerce.payment"),
1249 PathBuf::from("workers/commerce-payment/handlers.ts")
1250 );
1251 }
1252
1253 #[test]
1254 fn a_split_context_file_has_its_own_module_under_handlers() {
1255 assert_eq!(
1256 worker_file_source_path("shop.orders", Path::new("shop/orders/place.bynk")),
1257 PathBuf::from("workers/shop-orders/handlers/place.bynk")
1258 );
1259 }
1260
1261 #[test]
1263 fn unit_path_matches_single_file_layout() {
1264 assert!(unit_path_matches(Path::new("a/b/c.bynk"), "a.b.c"));
1265 assert!(unit_path_matches(Path::new("foo.bynk"), "foo"));
1266 }
1267
1268 #[test]
1269 fn unit_path_matches_multi_file_layout() {
1270 assert!(unit_path_matches(Path::new("a/b/c/handlers.bynk"), "a.b.c"));
1272 assert!(unit_path_matches(Path::new("a/b/c/anything.bynk"), "a.b.c"));
1273 }
1274
1275 #[test]
1276 fn unit_path_matches_rejects_misalignment() {
1277 assert!(!unit_path_matches(Path::new("a/b.bynk"), "a.b.c"));
1278 assert!(!unit_path_matches(Path::new("x/y/z.bynk"), "a.b.c"));
1279 }
1280
1281 #[test]
1283 fn is_multi_file_layout_true_only_for_directory_layout() {
1284 assert!(is_multi_file_layout(Path::new("thing/a.bynk"), "thing"));
1286 assert!(is_multi_file_layout(Path::new("thing/b.bynk"), "thing"));
1287 assert!(is_multi_file_layout(Path::new("a/b/one.bynk"), "a.b"));
1289 }
1290
1291 #[test]
1292 fn is_multi_file_layout_false_for_single_file_and_misalignment() {
1293 assert!(!is_multi_file_layout(Path::new("thing.bynk"), "thing"));
1295 assert!(!is_multi_file_layout(Path::new("a/b.bynk"), "a.b"));
1297 assert!(!is_multi_file_layout(Path::new("other/a.bynk"), "thing"));
1299 }
1300
1301 #[test]
1303 fn renamed_unit_name_single_file() {
1304 assert_eq!(
1305 renamed_unit_name(
1306 Path::new("a/b/c.bynk"),
1307 "a.b.c",
1308 Path::new("a/b/renamed.bynk")
1309 ),
1310 Some("a.b.renamed".to_string())
1311 );
1312 assert_eq!(
1313 renamed_unit_name(Path::new("foo.bynk"), "foo", Path::new("bar.bynk")),
1314 Some("bar".to_string())
1315 );
1316 }
1317
1318 #[test]
1319 fn renamed_unit_name_multi_file_member_rename_is_a_no_op() {
1320 assert_eq!(
1324 renamed_unit_name(
1325 Path::new("a/b/c/old.bynk"),
1326 "a.b.c",
1327 Path::new("a/b/c/new.bynk")
1328 ),
1329 Some("a.b.c".to_string())
1330 );
1331 }
1332
1333 #[test]
1334 fn renamed_unit_name_multi_file_directory_move() {
1335 assert_eq!(
1336 renamed_unit_name(
1337 Path::new("a/b/c/handlers.bynk"),
1338 "a.b.c",
1339 Path::new("a/b/renamed/handlers.bynk")
1340 ),
1341 Some("a.b.renamed".to_string())
1342 );
1343 }
1344
1345 #[test]
1346 fn renamed_unit_name_none_on_preexisting_misalignment() {
1347 assert_eq!(
1348 renamed_unit_name(Path::new("x/y/z.bynk"), "a.b.c", Path::new("x/y/w.bynk")),
1349 None
1350 );
1351 }
1352
1353 #[test]
1354 fn renamed_unit_name_tolerates_a_shared_include_root_prefix() {
1355 assert_eq!(
1361 renamed_unit_name(
1362 Path::new("src/billing/charge.bynk"),
1363 "billing.charge",
1364 Path::new("src/billing/pay.bynk")
1365 ),
1366 Some("billing.pay".to_string())
1367 );
1368 assert_eq!(
1370 renamed_unit_name(
1371 Path::new("src/a/b/c/handlers.bynk"),
1372 "a.b.c",
1373 Path::new("src/a/b/renamed/handlers.bynk")
1374 ),
1375 Some("a.b.renamed".to_string())
1376 );
1377 }
1378}
1379
1380#[cfg(test)]
1381mod manifest_tests {
1382 use super::*;
1383
1384 fn err(manifest: &str) -> String {
1385 check_manifest_str(manifest)
1386 .expect_err("the manifest is refused")
1387 .to_string()
1388 }
1389
1390 #[test]
1392 fn the_documented_tables_are_accepted() {
1393 check_manifest_str(
1394 "[project]\nname = \"p\"\nversion = \"0.1.0\"\n\n[paths]\ninclude = [\"src\"]\nexclude = []\n\n\
1395 [fmt]\nindent = \"tab\"\nmax_line_width = 100\n\n[lsp]\ndiagnostics_mode = \"live\"\n\
1396 diagnostics_debounce_ms = 300\n\n[workers]\ncompatibility_flags = [\"nodejs_compat\"]\n",
1397 )
1398 .expect("accepted");
1399 check_manifest_str("").expect("an empty manifest is accepted");
1400 }
1401
1402 #[test]
1403 fn a_planned_table_names_its_issue() {
1404 assert!(err("[deploy]\ngroups = []\n").contains("not yet supported (#551)"));
1405 }
1406
1407 #[test]
1408 fn a_typod_table_suggests_the_nearest() {
1409 assert!(err("[pahts]\n").contains("did you mean `[paths]`?"));
1410 assert!(err("[fnt]\n").contains("did you mean `[fmt]`?"));
1411 assert!(err("[zzzzzz]\n").contains("the tables are"));
1412 }
1413
1414 #[test]
1415 fn an_unknown_key_in_project_or_lsp_suggests_the_nearest() {
1416 assert!(err("[project]\nnmae = \"p\"\n").contains("did you mean `name`?"));
1417 assert!(
1418 err("[lsp]\ndiagnostics_mod = \"live\"\n").contains("did you mean `diagnostics_mode`?")
1419 );
1420 assert!(err("[project]\nlicense = \"MIT\"\n").contains("its keys are `name`, `version`"));
1421 }
1422
1423 #[test]
1424 fn a_paths_key_keeps_its_own_error() {
1425 assert!(matches!(
1426 check_manifest_str("[paths]\nout = \"out\"\n"),
1427 Err(ProjectPathsError::UnknownKey(k)) if k == "out"
1428 ));
1429 }
1430
1431 #[test]
1432 fn a_top_level_key_points_at_its_table() {
1433 assert!(err("name = \"p\"\n").contains("did you mean it under `[project]`?"));
1434 }
1435
1436 #[test]
1439 fn a_table_name_with_a_plain_value_must_be_a_table() {
1440 assert!(err("paths = \"src\"\n").contains("must be a table — write it as `[paths]`"));
1441 assert!(err("lsp = 3\n").contains("`[lsp]`"));
1442 assert!(err("fmt = true\n").contains("`[fmt]`"));
1443 check_manifest_str("paths = { include = [\"src\"] }\n").expect("accepted");
1445 }
1446
1447 #[test]
1449 fn a_planned_table_in_any_shape_names_its_issue() {
1450 assert!(err("[[deploy]]\nname = \"acme\"\n").contains("(#551)"));
1451 assert!(err("deploy = [\"acme\"]\n").contains("(#551)"));
1452 }
1453
1454 #[test]
1456 fn a_map_table_in_another_shape_must_be_a_table() {
1457 assert_eq!(
1458 err("[[dependencies]]\nname = \"acme\"\n"),
1459 "`dependencies` in `bynk.toml` must be a table — write it as `[dependencies]`"
1460 );
1461 assert_eq!(
1462 err("registries = [\"acme\"]\n"),
1463 "`registries` in `bynk.toml` must be a table — write it as `[registries]`"
1464 );
1465 assert_eq!(
1466 err("[dependencis]\n"),
1467 "`bynk.toml` has no table named `[dependencis]` — did you mean `[dependencies]`?"
1468 );
1469 }
1470
1471 #[test]
1473 fn fmt_keys_are_left_to_the_formatter() {
1474 check_manifest_str("[fmt]\nanything = 1\n").expect("not this check's concern");
1475 }
1476
1477 #[test]
1480 fn workers_compatibility_flags_must_be_a_list_of_strings() {
1481 let msg = "`[workers]` `compatibility_flags` must be a list of strings";
1482 assert!(err("[workers]\ncompatibility_flags = \"nodejs_compat\"\n").contains(msg));
1483 assert!(err("[workers]\ncompatibility_flags = [\"a\", 1]\n").contains(msg));
1484 assert!(err("[workers]\ncompatibility_flags = [[\"a\"]]\n").contains(msg));
1485 assert!(
1486 err("[workers]\ncompatibility_flag = []\n")
1487 .contains("did you mean `compatibility_flags`?")
1488 );
1489 check_manifest_str("[workers]\ncompatibility_flags = [\"no_such_flag_yet\"]\n")
1490 .expect("flag names are not validated");
1491 check_manifest_str("[workers]\n").expect("an empty `[workers]` is accepted");
1492 }
1493
1494 #[test]
1497 fn unstable_allow_must_be_a_list_of_strings() {
1498 let msg = "`[unstable]` `allow` must be a list of strings";
1499 assert!(err("[unstable]\nallow = \"bynk.cloudflare.D1\"\n").contains(msg));
1500 assert!(err("[unstable]\nallow = [\"a\", 1]\n").contains(msg));
1501 assert!(err("[unstable]\nallows = []\n").contains("did you mean `allow`?"));
1502 check_manifest_str("[unstable]\nallow = [\"bynk.anything\"]\n")
1503 .expect("item paths are the checker's to validate");
1504 check_manifest_str("[unstable]\n").expect("an empty `[unstable]` is accepted");
1505 }
1506
1507 #[test]
1508 fn unstable_config_reads_the_entries_in_order() {
1509 let root = PathBuf::from("/nonexistent-bynk-test-root-1963");
1510 let read = |manifest: &str| {
1511 let overlay = HashMap::from([(root.join("bynk.toml"), manifest.to_string())]);
1512 try_read_unstable_config_with(&root, &overlay)
1513 };
1514 assert_eq!(
1515 read("[unstable]\nallow = [\"b.x\", \"a.y\"]\n")
1516 .unwrap()
1517 .allow,
1518 vec!["b.x", "a.y"]
1519 );
1520 assert_eq!(
1521 read("[project]\nname = \"p\"\n").unwrap(),
1522 UnstableConfig::default()
1523 );
1524 assert!(matches!(
1525 read("[unstable]\nallow = 3\n"),
1526 Err(ProjectPathsError::NotAStringList {
1527 table: "unstable",
1528 key: "allow"
1529 })
1530 ));
1531 }
1532
1533 #[test]
1534 fn workers_config_reads_the_flags_in_order() {
1535 let root = PathBuf::from("/nonexistent-bynk-test-root-1890");
1536 let read = |manifest: &str| {
1537 let overlay = HashMap::from([(root.join("bynk.toml"), manifest.to_string())]);
1538 try_read_workers_config_with(&root, &overlay)
1539 };
1540 assert_eq!(
1541 read("[workers]\ncompatibility_flags = [\"b\", \"a\", \"b\"]\n")
1542 .unwrap()
1543 .compatibility_flags,
1544 vec!["b", "a", "b"],
1545 "order and duplicates are kept; deduplication is the emitter's"
1546 );
1547 assert_eq!(
1548 read("[project]\nname = \"p\"\n").unwrap(),
1549 WorkersConfig::default()
1550 );
1551 assert_eq!(
1552 try_read_workers_config_with(&root, &HashMap::new()).unwrap(),
1553 WorkersConfig::default(),
1554 "no manifest at all"
1555 );
1556 assert!(matches!(
1557 read("[workers]\ncompatibility_flags = [true]\n"),
1558 Err(ProjectPathsError::NotAStringList { .. })
1559 ));
1560 assert!(matches!(
1561 read("[workers\n"),
1562 Err(ProjectPathsError::Malformed)
1563 ));
1564 }
1565
1566 #[test]
1567 fn a_malformed_manifest_is_malformed() {
1568 assert!(matches!(
1569 check_manifest_str("[project\n"),
1570 Err(ProjectPathsError::Malformed)
1571 ));
1572 }
1573
1574 #[test]
1579 fn the_readers_accept_the_shapes_the_manifest_schema_offers() {
1580 let root = PathBuf::from("/nonexistent-bynk-test-root-2096");
1581 let overlay =
1582 |manifest: &str| HashMap::from([(root.join("bynk.toml"), manifest.to_string())]);
1583
1584 for manifest in [
1586 "[paths]\ninclude = \"src\"\n",
1587 "[paths]\ninclude = [\"src\"]\n",
1588 ] {
1589 check_manifest_str(manifest).expect("the manifest check accepts it");
1590 let paths = try_read_project_paths_with(&root, &overlay(manifest))
1591 .unwrap_or_else(|e| panic!("{manifest:?}: {e}"));
1592 assert_eq!(paths.include, vec![PathBuf::from("src")], "{manifest:?}");
1593 }
1594
1595 let manifest = "[workers]\ncompatibility_flags = [\"x\"]\n";
1597 check_manifest_str(manifest).expect("the manifest check accepts it");
1598 assert_eq!(
1599 try_read_workers_config_with(&root, &overlay(manifest))
1600 .unwrap()
1601 .compatibility_flags,
1602 vec!["x"]
1603 );
1604 let manifest = "[unstable]\nallow = [\"x\"]\n";
1605 check_manifest_str(manifest).expect("the manifest check accepts it");
1606 assert_eq!(
1607 try_read_unstable_config_with(&root, &overlay(manifest))
1608 .unwrap()
1609 .allow,
1610 vec!["x"]
1611 );
1612
1613 let manifest = "[workers]\ncompatibility_flags = \"x\"\n";
1616 assert!(matches!(
1617 try_read_workers_config_with(&root, &overlay(manifest)),
1618 Err(ProjectPathsError::NotAStringList { .. })
1619 ));
1620 assert!(check_manifest_str(manifest).is_err());
1621 let manifest = "[unstable]\nallow = \"x\"\n";
1622 assert!(matches!(
1623 try_read_unstable_config_with(&root, &overlay(manifest)),
1624 Err(ProjectPathsError::NotAStringList { .. })
1625 ));
1626 assert!(check_manifest_str(manifest).is_err());
1627
1628 let manifest = "[test.targets.preview]\nurl.shop-orders = \"https://x\"\nsecrets = [\"S\"]\nvars = { P = \"fake\" }\nproduction = false\n";
1630 check_manifest_str(manifest).expect("a well-formed target is accepted");
1631 let targets = test_targets_from(
1632 manifest.parse::<toml::Table>().unwrap()["test"]
1633 .as_table()
1634 .unwrap(),
1635 )
1636 .unwrap();
1637 let preview = &targets["preview"];
1638 assert_eq!(preview.urls["shop-orders"], "https://x");
1639 assert_eq!(preview.secrets, ["S"]);
1640 assert_eq!(preview.vars["P"], "fake");
1641 assert!(!preview.production);
1642 for (manifest, needle) in [
1643 ("[test.targets.p]\nsecrets = []\n", "names no Worker URL"),
1644 (
1645 "[test.targets.p]\nurl.a = \"x\"\nsecret = \"S\"\n",
1646 "no key named `secret`",
1647 ),
1648 (
1649 "[test.targets.p]\nurl.a = \"x\"\nproduction = \"yes\"\n",
1650 "`production` must be",
1651 ),
1652 ("[test.targets.p]\nurl.a = 1\n", "`url.a` must be a string"),
1653 ("[test]\ntarget = 1\n", "no key named `target`"),
1654 ] {
1655 let err = check_manifest_str(manifest)
1656 .expect_err(manifest)
1657 .to_string();
1658 assert!(err.contains(needle), "{manifest:?}: {err}");
1659 }
1660 }
1661}