diff --git a/src/Composer/Json/JsonManipulator.php b/src/Composer/Json/JsonManipulator.php new file mode 100644 index 000000000..f070fe792 --- /dev/null +++ b/src/Composer/Json/JsonManipulator.php @@ -0,0 +1,120 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Json; + +/** + * @author Jordi Boggiano + */ +class JsonManipulator +{ + private $contents; + private $newline; + private $indent; + + public function __construct($contents) + { + if (!preg_match('#^\{(.*)\}$#s', trim($contents), $match)) { + throw new \InvalidArgumentException('The json file must be an object ({})'); + } + $this->newline = false !== strpos("\r\n", $contents) ? "\r\n": "\n"; + $this->contents = '{' . $match[1] . '}'; + $this->detectIndenting(); + } + + public function getContents() + { + return $this->contents . $this->newline; + } + + public function addLink($type, $package, $constraint) + { + // no link of that type yet + if (!preg_match('#"'.$type.'":\s*\{#', $this->contents)) { + $this->addMainKey($type, $this->format(array($package => $constraint))); + + return true; + } + + $linksRegex = '#("'.$type.'":\s*\{)([^}]+)(\})#s'; + if (!preg_match($linksRegex, $this->contents, $match)) { + return false; + } + + $links = $match[2]; + $packageRegex = str_replace('/', '\\\\?/', preg_quote($package)); + + // link exists already + if (preg_match('{"'.$packageRegex.'"\s*:}i', $links)) { + $links = preg_replace('{"'.$packageRegex.'"(\s*:\s*)"[^"]+"}i', JsonFile::encode($package).'$1"'.$constraint.'"', $links); + } elseif (preg_match('#[^\s](\s*)$#', $links, $match)) { + // link missing but non empty links + $links = preg_replace( + '#'.$match[1].'$#', + ',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $match[1], + $links + ); + } else { + // links empty + $links = $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $links; + } + + $this->contents = preg_replace($linksRegex, '$1'.$links.'$3', $this->contents); + + return true; + } + + public function addMainKey($key, $content) + { + if (preg_match('#[^{\s](\s*)\}$#', $this->contents, $match)) { + $this->contents = preg_replace( + '#'.$match[1].'\}$#', + ',' . $this->newline . $this->indent . JsonFile::encode($key). ': '. $content . $this->newline . '}', + $this->contents + ); + } else { + $this->contents = preg_replace( + '#\}$#', + $this->indent . JsonFile::encode($key). ': '.$content . $this->newline . '}', + $this->contents + ); + } + } + + protected function format($data) + { + if (is_array($data)) { + reset($data); + + if (is_numeric(key($data))) { + return '['.implode(', ', $data).']'; + } + + $out = '{' . $this->newline; + foreach ($data as $key => $val) { + $elems[] = $this->indent . $this->indent . JsonFile::encode($key). ': '.$this->format($val); + } + return $out . implode(','.$this->newline, $elems) . $this->newline . $this->indent . '}'; + } + + return JsonFile::encode($data); + } + + protected function detectIndenting() + { + if (preg_match('{^(\s+)"}', $this->contents, $match)) { + $this->indent = $match[1]; + } else { + $this->indent = ' '; + } + } +} diff --git a/tests/Composer/Test/Json/JsonManipulatorTest.php b/tests/Composer/Test/Json/JsonManipulatorTest.php new file mode 100644 index 000000000..e6718b56a --- /dev/null +++ b/tests/Composer/Test/Json/JsonManipulatorTest.php @@ -0,0 +1,134 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Json; + +use Composer\Json\JsonManipulator; + +class JsonManipulatorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider linkProvider + */ + public function testAddLink($json, $type, $package, $constraint, $expected) + { + $manipulator = new JsonManipulator($json); + $this->assertTrue($manipulator->addLink($type, $package, $constraint)); + $this->assertEquals($expected, $manipulator->getContents()); + } + + public function linkProvider() + { + return array( + array( + '{ +}', + 'require', + 'vendor/baz', + 'qux', + '{ + "require": { + "vendor/baz": "qux" + } +} +' + ), + array( + '{ + "foo": "bar" +}', + 'require', + 'vendor/baz', + 'qux', + '{ + "foo": "bar", + "require": { + "vendor/baz": "qux" + } +} +' + ), + array( + '{ + "require": { + } +}', + 'require', + 'vendor/baz', + 'qux', + '{ + "require": { + "vendor/baz": "qux" + } +} +' + ), + array( + '{ + "require": { + "foo": "bar" + } +}', + 'require', + 'vendor/baz', + 'qux', + '{ + "require": { + "foo": "bar", + "vendor/baz": "qux" + } +} +' + ), + array( + '{ + "require": + { + "foo": "bar", + "vendor/baz": "baz" + } +}', + 'require', + 'vendor/baz', + 'qux', + '{ + "require": + { + "foo": "bar", + "vendor/baz": "qux" + } +} +' + ), + array( + '{ + "require": + { + "foo": "bar", + "vendor\/baz": "baz" + } +}', + 'require', + 'vendor/baz', + 'qux', + '{ + "require": + { + "foo": "bar", + "vendor/baz": "qux" + } +} +' + ), + ); + } +}