1use crate::constraint::{parse_constraints, Constraint};
27use crate::intervals::is_subset_of;
28use serde_json::{Map, Value};
29use std::collections::BTreeMap;
30use std::path::Path;
31use vivacity_core::glob::glob_plain;
32use vivacity_core::phparray::{array_merge, array_merge_recursive, merge_deep};
33
34pub const PLUGIN_NAME: &str = "wikimedia/composer-merge-plugin";
35
36#[derive(Debug, Clone)]
38pub struct Settings {
39 pub includes: Vec<String>,
40 pub requires: Vec<String>,
41 pub recurse: bool,
42 pub replace: bool,
43 pub ignore_duplicates: bool,
44 pub merge_dev: bool,
45 pub merge_extra: bool,
46 pub merge_extra_deep: bool,
47 pub merge_replace: bool,
48}
49
50fn patterns(v: Option<&Value>) -> Vec<String> {
51 match v {
52 Some(Value::String(s)) => vec![s.clone()],
53 Some(Value::Array(a)) => a
54 .iter()
55 .filter_map(Value::as_str)
56 .map(str::to_owned)
57 .collect(),
58 _ => Vec::new(),
59 }
60}
61
62fn flag(cfg: Option<&Value>, key: &str, default: bool) -> bool {
63 match cfg.and_then(|c| c.get(key)) {
65 None | Some(Value::Null) => default,
66 Some(Value::Bool(b)) => *b,
67 Some(Value::Number(n)) => n.as_f64() != Some(0.0),
68 Some(Value::String(s)) => !(s.is_empty() || s == "0"),
69 Some(Value::Array(a)) => !a.is_empty(),
70 Some(Value::Object(o)) => !o.is_empty(),
71 }
72}
73
74impl Settings {
75 pub fn from_manifest(manifest: &Value) -> Settings {
76 let cfg = manifest.get("extra").and_then(|e| e.get("merge-plugin"));
77 Settings {
78 includes: patterns(cfg.and_then(|c| c.get("include"))),
79 requires: patterns(cfg.and_then(|c| c.get("require"))),
80 recurse: flag(cfg, "recurse", true),
81 replace: flag(cfg, "replace", false),
82 ignore_duplicates: flag(cfg, "ignore-duplicates", false),
83 merge_dev: flag(cfg, "merge-dev", true),
84 merge_extra: flag(cfg, "merge-extra", false),
85 merge_extra_deep: flag(cfg, "merge-extra-deep", false),
86 merge_replace: flag(cfg, "merge-replace", true),
87 }
88 }
89
90 pub fn declared(manifest: &Value) -> bool {
92 let s = Settings::from_manifest(manifest);
93 !(s.includes.is_empty() && s.requires.is_empty())
94 }
95}
96
97#[derive(Debug, Clone, PartialEq)]
99pub struct MergedLink {
100 pub target: String,
101 pub constraint: Constraint,
102 pub pretty: String,
104 pub text: String,
107}
108
109#[derive(Debug, Clone)]
111pub struct Merged {
112 pub manifest: Value,
116 pub requires: Vec<MergedLink>,
117 pub requires_dev: Vec<MergedLink>,
118 pub merged_names: Vec<String>,
120 pub files: Vec<String>,
122}
123
124struct Merger<'a> {
125 project_dir: &'a Path,
126 settings: Settings,
127 with_dev: bool,
128 root_version: String,
129 root_pretty_version: String,
130 root: Value,
131 requires: BTreeMap<String, MergedLink>,
132 requires_dev: BTreeMap<String, MergedLink>,
133 require_order: Vec<String>,
136 require_dev_order: Vec<String>,
137 merged_names: Vec<String>,
138 loaded: Vec<String>,
139 files: Vec<String>,
140}
141
142pub fn merge(
146 project_dir: &Path,
147 manifest: &Value,
148 root_version: &str,
149 root_pretty_version: &str,
150 with_dev: bool,
151) -> Result<Merged, String> {
152 let settings = Settings::from_manifest(manifest);
153 let mut m = Merger {
154 project_dir,
155 settings: settings.clone(),
156 with_dev: with_dev && settings.merge_dev,
157 root_version: root_version.to_owned(),
158 root_pretty_version: root_pretty_version.to_owned(),
159 root: manifest.clone(),
160 requires: BTreeMap::new(),
161 requires_dev: BTreeMap::new(),
162 require_order: Vec::new(),
163 require_dev_order: Vec::new(),
164 merged_names: Vec::new(),
165 loaded: Vec::new(),
166 files: Vec::new(),
167 };
168 for key in ["require", "require-dev"] {
169 let (map, order) = if key == "require" {
170 (&mut m.requires, &mut m.require_order)
171 } else {
172 (&mut m.requires_dev, &mut m.require_dev_order)
173 };
174 if let Some(links) = manifest.get(key).and_then(Value::as_object) {
175 for (target, pretty) in links {
176 let Some(pretty) = pretty.as_str() else {
177 continue;
178 };
179 let name = target.to_lowercase();
180 let source = if pretty == "self.version" {
183 root_version
184 } else {
185 pretty
186 };
187 let parsed = parse_constraints(source)
188 .map_err(|e| format!("composer.json {key}.{target}: {e}"))?;
189 map.insert(
190 name.clone(),
191 MergedLink {
192 target: name.clone(),
193 constraint: parsed.constraint,
194 pretty: pretty.to_owned(),
195 text: pretty.to_owned(),
196 },
197 );
198 order.push(name);
199 }
200 }
201 }
202 let includes = settings.includes.clone();
203 let requires = settings.requires.clone();
204 m.merge_files(&includes, false)?;
205 m.merge_files(&requires, true)?;
206 for (key, map, order) in [
208 ("require", &m.requires, &m.require_order),
209 ("require-dev", &m.requires_dev, &m.require_dev_order),
210 ] {
211 if map.is_empty() {
212 continue;
213 }
214 let mut out = Map::new();
215 for name in order {
216 if let Some(l) = map.get(name) {
217 out.insert(name.clone(), Value::String(l.text.clone()));
218 }
219 }
220 m.root[key] = Value::Object(out);
221 }
222 let mut merged_names: Vec<String> = Vec::new();
224 for n in m.merged_names {
225 if !merged_names.contains(&n) {
226 merged_names.push(n);
227 }
228 }
229 Ok(Merged {
230 manifest: m.root,
231 requires: m
232 .require_order
233 .iter()
234 .filter_map(|n| m.requires.get(n).cloned())
235 .collect(),
236 requires_dev: m
237 .require_dev_order
238 .iter()
239 .filter_map(|n| m.requires_dev.get(n).cloned())
240 .collect(),
241 merged_names,
242 files: m.files,
243 })
244}
245
246impl Merger<'_> {
247 fn merge_files(&mut self, patterns: &[String], required: bool) -> Result<(), String> {
250 let mut paths: Vec<String> = Vec::new();
251 for pattern in patterns {
252 let found = glob_plain(pattern, self.project_dir);
253 if required && found.is_empty() {
254 return Err(format!(
255 "merge-plugin: No files matched required '{pattern}'"
256 ));
257 }
258 paths.extend(found);
259 }
260 for path in paths {
261 self.merge_file(&path)?;
262 }
263 Ok(())
264 }
265
266 fn merge_file(&mut self, path: &str) -> Result<(), String> {
268 if self.loaded.iter().any(|p| p == path) {
269 return Ok(());
270 }
271 let text = std::fs::read_to_string(self.project_dir.join(path))
272 .map_err(|e| format!("merge-plugin: {path}: {e}"))?;
273 let json: Value = serde_json::from_str(&text)
274 .map_err(|e| format!("merge-plugin: \"{path}\" does not contain valid JSON\n{e}"))?;
275 if matches!(&json, Value::Array(a) if a.is_empty()) {
277 self.loaded.push(path.to_owned());
278 return Ok(());
279 }
280 if !json.is_object() {
281 return Err(format!(
282 "merge-plugin: \"{path}\" does not contain valid JSON"
283 ));
284 }
285 let base = {
286 let d = vivacity_core::pathutil::php_dirname(path);
287 if d == "." {
288 String::new()
289 } else {
290 format!("{d}/")
291 }
292 };
293 let name = json
294 .get("name")
295 .and_then(Value::as_str)
296 .map(str::to_owned)
297 .unwrap_or_else(|| format!("merge-plugin/{}", path.replace('/', "-")));
298 self.files.push(path.to_owned());
299 self.loaded.push(path.to_owned());
300
301 self.merge_requires("require", &json, &name, path)?;
303 self.merge_package_links("conflict", &json, &name, path)?;
304 if self.settings.merge_replace {
305 self.merge_package_links("replace", &json, &name, path)?;
306 }
307 self.merge_package_links("provide", &json, &name, path)?;
308 self.merge_autoload("autoload", &json, &base);
309 self.merge_extra(&json);
310 if self.with_dev {
311 self.merge_requires("require-dev", &json, &name, path)?;
313 self.merge_autoload("autoload-dev", &json, &base);
314 }
315 if self.settings.recurse {
316 let inner = Settings::from_manifest(&json);
317 let rebase = |p: &String| format!("{base}{p}");
318 let includes: Vec<String> = inner.includes.iter().map(rebase).collect();
319 let requires: Vec<String> = inner.requires.iter().map(rebase).collect();
320 self.merge_files(&includes, false)?;
321 self.merge_files(&requires, true)?;
322 }
323 Ok(())
324 }
325
326 fn self_version(
330 &self,
331 key: &str,
332 source: &str,
333 pretty: &str,
334 path: &str,
335 ) -> Result<(Constraint, String), String> {
336 let parse = |text: &str| {
338 parse_constraints(text)
339 .map(|p| p.constraint)
340 .map_err(|e| format!("merge-plugin: {path} {key}: {e}"))
341 };
342 if pretty != "self.version" {
343 return Ok((parse(pretty)?, pretty.to_owned()));
344 }
345 let lower = source.to_lowercase();
348 let existing: Option<String> = match key {
349 "require" => self.requires.get(&lower).map(|l| l.pretty.clone()),
350 "require-dev" => self.requires_dev.get(&lower).map(|l| l.pretty.clone()),
351 _ => self
352 .root
353 .get(key)
354 .and_then(Value::as_object)
355 .and_then(|m| m.get(&lower))
356 .and_then(Value::as_str)
357 .map(str::to_owned),
358 };
359 if let Some(existing) = existing {
360 return Ok((parse(&existing)?, existing));
361 }
362 Ok((parse(&self.root_version)?, self.root_pretty_version.clone()))
363 }
364
365 fn merge_requires(
367 &mut self,
368 key: &str,
369 json: &Value,
370 source: &str,
371 path: &str,
372 ) -> Result<(), String> {
373 let Some(links) = json.get(key).and_then(Value::as_object) else {
374 return Ok(());
375 };
376 if links.is_empty() {
377 return Ok(());
378 }
379 let ignore = self.settings.ignore_duplicates;
380 let replace = self.settings.replace;
381 for (target, pretty) in links {
382 let Some(pretty) = pretty.as_str() else {
383 continue;
384 };
385 let name = target.to_lowercase();
386 let (constraint, pretty) = self.self_version(key, source, pretty, path)?;
387 let incoming = MergedLink {
388 target: name.clone(),
389 constraint,
390 text: pretty.clone(),
391 pretty,
392 };
393 let (map, order) = if key == "require" {
394 (&mut self.requires, &mut self.require_order)
395 } else {
396 (&mut self.requires_dev, &mut self.require_dev_order)
397 };
398 match map.get(&name) {
399 Some(_) if ignore => {}
400 Some(_) if replace => {
401 map.insert(name.clone(), incoming);
402 self.merged_names.push(name);
403 }
404 Some(origin) => {
405 let merged = merge_constraints(origin, &incoming);
406 map.insert(name.clone(), merged);
407 self.merged_names.push(name);
408 }
409 None => {
410 map.insert(name.clone(), incoming);
411 order.push(name.clone());
412 self.merged_names.push(name);
413 }
414 }
415 }
416 Ok(())
417 }
418
419 fn merge_package_links(
423 &mut self,
424 key: &str,
425 json: &Value,
426 source: &str,
427 path: &str,
428 ) -> Result<(), String> {
429 let Some(links) = json.get(key).and_then(Value::as_object) else {
430 return Ok(());
431 };
432 if links.is_empty() {
433 return Ok(());
434 }
435
436 let mut out: Map<String, Value> = self
437 .root
438 .get(key)
439 .and_then(Value::as_object)
440 .cloned()
441 .unwrap_or_default();
442 for (target, pretty) in links {
443 let Some(pretty) = pretty.as_str() else {
444 continue;
445 };
446 let (_, pretty) = self.self_version(key, source, pretty, path)?;
447 let name = target.to_lowercase();
448 if let Some(existing) = out.keys().find(|k| k.to_lowercase() == name).cloned() {
450 out.insert(existing, Value::String(pretty));
451 } else {
452 out.insert(name, Value::String(pretty));
453 }
454 }
455 self.root[key] = Value::Object(out);
456 Ok(())
457 }
458
459 fn merge_autoload(&mut self, key: &str, json: &Value, base: &str) {
462 let Some(autoload) = json.get(key) else {
463 return;
464 };
465 if !autoload.is_object() || autoload.as_object().is_some_and(|o| o.is_empty()) {
466 return;
467 }
468 let rebased = rebase(autoload, base);
469 let current = self
470 .root
471 .get(key)
472 .cloned()
473 .unwrap_or_else(|| Value::Object(Map::new()));
474 self.root[key] = array_merge_recursive(¤t, &rebased);
475 }
476
477 fn merge_extra(&mut self, json: &Value) {
481 let Some(extra) = json.get("extra").and_then(Value::as_object) else {
482 return;
483 };
484 let mut extra = extra.clone();
485 extra.remove("merge-plugin");
486 if !self.settings.merge_extra || extra.is_empty() {
487 return;
488 }
489 let extra = Value::Object(extra);
490 let root_extra = self
491 .root
492 .get("extra")
493 .cloned()
494 .unwrap_or_else(|| Value::Object(Map::new()));
495 let (first, second) = if self.settings.replace {
496 (&root_extra, &extra)
497 } else {
498 (&extra, &root_extra)
499 };
500 self.root["extra"] = if self.settings.merge_extra_deep {
501 merge_deep(first, second)
502 } else {
503 array_merge(first, second)
504 };
505 }
506}
507
508fn merge_constraints(origin: &MergedLink, incoming: &MergedLink) -> MergedLink {
511 if is_subset_of(&origin.constraint, &incoming.constraint) {
512 return origin.clone();
513 }
514 if is_subset_of(&incoming.constraint, &origin.constraint) {
515 return incoming.clone();
516 }
517 MergedLink {
518 target: origin.target.clone(),
519 constraint: Constraint::create(
520 vec![origin.constraint.clone(), incoming.constraint.clone()],
521 true,
522 ),
523 pretty: format!("{}, {}", origin.pretty, incoming.pretty),
524 text: conjunction_text(&origin.text, &incoming.text),
525 }
526}
527
528fn rebase(v: &Value, base: &str) -> Value {
531 match v {
532 Value::String(s) => Value::String(format!("{base}{s}")),
533 Value::Array(a) => Value::Array(a.iter().map(|x| rebase(x, base)).collect()),
534 Value::Object(o) => Value::Object(
535 o.iter()
536 .map(|(k, x)| (k.clone(), rebase(x, base)))
537 .collect(),
538 ),
539 other => other.clone(),
540 }
541}
542
543fn conjunction_text(a: &str, b: &str) -> String {
549 let mut out = Vec::new();
550 for x in split_or(a) {
551 for y in split_or(b) {
552 out.push(format!("{x}, {y}"));
553 }
554 }
555 out.join(" || ")
556}
557
558fn split_or(text: &str) -> Vec<&str> {
560 text.split("||")
561 .flat_map(|s| s.split('|'))
562 .map(str::trim)
563 .filter(|s| !s.is_empty())
564 .collect()
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570 use serde_json::json;
571
572 fn write(dir: &Path, rel: &str, v: &Value) {
573 let p = dir.join(rel);
574 std::fs::create_dir_all(p.parent().unwrap()).unwrap();
575 std::fs::write(p, serde_json::to_string_pretty(v).unwrap()).unwrap();
576 }
577
578 fn root() -> Value {
579 json!({
580 "name": "acme/app",
581 "require": {"monolog/monolog": "^2.8", "psr/log": "^3.0"},
582 "require-dev": {"phpunit/phpunit": "^10"},
583 "autoload": {"psr-4": {"App\\": "src/"}, "files": ["helpers.php"]},
584 "conflict": {"old/lib": "<1.0"},
585 "extra": {"merge-plugin": {"include": ["modules/*/composer.json", "composer.ext.json"], "require": ["required/composer.json"]}, "keep": 1, "shared": {"a": 1}}
586 })
587 }
588
589 #[test]
590 fn merges_every_section_like_the_plugin() {
591 let tmp = tempfile::tempdir().unwrap();
592 let d = tmp.path();
593 write(
594 d,
595 "modules/Base/composer.json",
596 &json!({
597 "name": "acme/base",
598 "require": {"monolog/monolog": "^2.11 || ^1.0", "guzzlehttp/guzzle": "^7.5", "acme/self": "self.version"},
599 "require-dev": {"mockery/mockery": "^1.6"},
600 "autoload": {"psr-4": {"App\\": "src/", "Base\\": "lib/"}, "files": ["boot.php"], "exclude-from-classmap": ["tests/"]},
601 "autoload-dev": {"psr-4": {"BaseTests\\": "tests/"}},
602 "provide": {"psr/log-implementation": "3.0"},
603 "replace": {"acme/base-compat": "self.version"},
604 "conflict": {"old/lib": "<2.0", "bad/lib": "*"},
605 "extra": {"merge-plugin": {"include": ["nested/composer.json"]}, "shared": {"b": 2}, "only-include": true}
606 }),
607 );
608 write(
609 d,
610 "modules/Base/nested/composer.json",
611 &json!({
612 "require": {"nested/dep": "^1.0"},
613 "autoload": {"classmap": ["Legacy/"]}
614 }),
615 );
616 write(
617 d,
618 "composer.ext.json",
619 &json!({"require": {"psr/log": "^3.0"}}),
620 );
621 write(
622 d,
623 "required/composer.json",
624 &json!({"require": {"symfony/yaml": "^7.4"}}),
625 );
626 let m = merge(d, &root(), "1.2.3.0", "1.2.3", true).unwrap();
627 assert_eq!(
628 m.files,
629 vec![
630 "modules/Base/composer.json",
631 "modules/Base/nested/composer.json",
632 "composer.ext.json",
633 "required/composer.json"
634 ]
635 );
636 let mono = m
638 .requires
639 .iter()
640 .find(|l| l.target == "monolog/monolog")
641 .unwrap();
642 assert_eq!(mono.pretty, "^2.8, ^2.11 || ^1.0");
643 assert!(mono.constraint.matches_version("2.11.0.0"));
644 assert!(
645 !mono.constraint.matches_version("1.5.0.0"),
646 "1.x is outside ^2.8"
647 );
648 assert!(!mono.constraint.matches_version("2.10.0.0"), "below ^2.11");
649 assert_eq!(
651 m.manifest["require"]["monolog/monolog"],
652 json!("^2.8, ^2.11 || ^2.8, ^1.0")
653 );
654 let reparsed = parse_constraints("^2.8, ^2.11 || ^2.8, ^1.0")
655 .unwrap()
656 .constraint;
657 for v in ["2.11.0.0", "2.10.0.0", "1.5.0.0", "3.0.0.0"] {
658 assert_eq!(
659 reparsed.matches_version(v),
660 mono.constraint.matches_version(v),
661 "{v}"
662 );
663 }
664 assert_eq!(m.manifest["require"]["psr/log"], json!("^3.0"));
666 assert_eq!(m.manifest["require"]["acme/self"], json!("1.2.3"));
668 assert_eq!(m.manifest["replace"]["acme/base-compat"], json!("1.2.3"));
669 let order: Vec<&str> = m.requires.iter().map(|l| l.target.as_str()).collect();
671 assert_eq!(
672 order,
673 vec![
674 "monolog/monolog",
675 "psr/log",
676 "guzzlehttp/guzzle",
677 "acme/self",
678 "nested/dep",
679 "symfony/yaml"
680 ]
681 );
682 assert_eq!(
683 m.requires_dev
684 .iter()
685 .map(|l| l.target.as_str())
686 .collect::<Vec<_>>(),
687 vec!["phpunit/phpunit", "mockery/mockery"]
688 );
689 assert_eq!(
691 m.merged_names,
692 vec![
693 "monolog/monolog",
694 "guzzlehttp/guzzle",
695 "acme/self",
696 "mockery/mockery",
697 "nested/dep",
698 "psr/log",
699 "symfony/yaml"
700 ]
701 );
702 assert_eq!(
704 m.manifest["autoload"],
705 json!({
706 "psr-4": {"App\\": ["src/", "modules/Base/src/"], "Base\\": "modules/Base/lib/"},
707 "files": ["helpers.php", "modules/Base/boot.php"],
708 "exclude-from-classmap": ["modules/Base/tests/"],
709 "classmap": ["modules/Base/nested/Legacy/"]
710 })
711 );
712 assert_eq!(
713 m.manifest["autoload-dev"],
714 json!({"psr-4": {"BaseTests\\": "modules/Base/tests/"}})
715 );
716 assert_eq!(
718 m.manifest["conflict"],
719 json!({"old/lib": "<2.0", "bad/lib": "*"})
720 );
721 assert_eq!(
722 m.manifest["provide"],
723 json!({"psr/log-implementation": "3.0"})
724 );
725 assert_eq!(m.manifest["extra"]["keep"], json!(1));
727 assert!(m.manifest["extra"].get("only-include").is_none());
728 }
729
730 #[test]
731 fn duplicate_rules_dev_off_and_extra() {
732 let tmp = tempfile::tempdir().unwrap();
733 let d = tmp.path();
734 write(
735 d,
736 "inc/composer.json",
737 &json!({
738 "require": {"monolog/monolog": "^2.11"},
739 "require-dev": {"x/y": "*"},
740 "autoload-dev": {"psr-4": {"T\\": "t/"}},
741 "extra": {"shared": {"b": 2}, "keep": 2, "new": 3}
742 }),
743 );
744 let mut r = root();
745 r["extra"]["merge-plugin"] = json!({"include": ["inc/composer.json"], "ignore-duplicates": true, "merge-dev": false, "merge-extra": true});
746 let m = merge(d, &r, "1.0.0.0", "1.0.0", true).unwrap();
747 assert_eq!(
748 m.manifest["require"]["monolog/monolog"],
749 json!("^2.8"),
750 "ignored duplicate"
751 );
752 assert!(m.merged_names.is_empty());
753 assert!(
754 m.requires_dev.iter().all(|l| l.target != "x/y"),
755 "merge-dev false"
756 );
757 assert!(m.manifest.get("autoload-dev").is_none());
758 assert_eq!(m.manifest["extra"]["keep"], json!(1));
760 assert_eq!(m.manifest["extra"]["shared"], json!({"a": 1}));
761 assert_eq!(m.manifest["extra"]["new"], json!(3));
762 r["extra"]["merge-plugin"] = json!({"include": ["inc/composer.json"], "replace": true, "merge-extra": true, "merge-extra-deep": true});
764 let m = merge(d, &r, "1.0.0.0", "1.0.0", false).unwrap();
765 assert_eq!(m.manifest["require"]["monolog/monolog"], json!("^2.11"));
766 assert_eq!(m.manifest["extra"]["keep"], json!(2));
767 assert_eq!(m.manifest["extra"]["shared"], json!({"a": 1, "b": 2}));
768 write(
770 d,
771 "inc/composer.json",
772 &json!({"require": {"monolog/monolog": "^2.9"}}),
773 );
774 r["extra"]["merge-plugin"] = json!({"include": ["inc/composer.json"]});
775 let m = merge(d, &r, "1.0.0.0", "1.0.0", false).unwrap();
776 assert_eq!(m.manifest["require"]["monolog/monolog"], json!("^2.9"));
777 }
778
779 #[test]
780 fn required_pattern_must_match_and_invalid_json_fails() {
781 let tmp = tempfile::tempdir().unwrap();
782 let d = tmp.path();
783 let mut r = root();
784 r["extra"]["merge-plugin"] = json!({"require": ["nowhere/*.json"]});
785 assert_eq!(
786 merge(d, &r, "1.0.0.0", "1.0.0", true).unwrap_err(),
787 "merge-plugin: No files matched required 'nowhere/*.json'"
788 );
789 std::fs::create_dir_all(d.join("inc")).unwrap();
790 std::fs::write(d.join("inc/composer.json"), "{ nope").unwrap();
791 r["extra"]["merge-plugin"] = json!({"include": "inc/composer.json"});
792 assert!(merge(d, &r, "1.0.0.0", "1.0.0", true)
793 .unwrap_err()
794 .contains("does not contain valid JSON"));
795 std::fs::write(
797 d.join("inc/composer.json"),
798 r#"{"require": {"x/y": "^^1"}}"#,
799 )
800 .unwrap();
801 assert!(merge(d, &r, "1.0.0.0", "1.0.0", true)
802 .unwrap_err()
803 .contains("inc/composer.json require"));
804 std::fs::write(d.join("inc/composer.json"), "[]").unwrap();
806 let mut r2 = r.clone();
807 r2["require"]["acme/app-alias"] = json!("self.version");
808 let m = merge(d, &r2, "1.0.0.0", "1.0.0", true).unwrap();
809 let l = m
810 .requires
811 .iter()
812 .find(|l| l.target == "acme/app-alias")
813 .unwrap();
814 assert_eq!(l.pretty, "self.version");
815 assert!(l.constraint.matches_version("1.0.0.0"));
816 r["extra"]["merge-plugin"] = json!({"include": ["absent/*.json"]});
818 let m = merge(d, &r, "1.0.0.0", "1.0.0", true).unwrap();
819 assert_eq!(m.manifest, r);
820 assert!(m.files.is_empty());
821 }
822}