Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Ability to use Command Line Options in jq #15

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ json-query-wrapper is a wrapper for the popular command-line JSON processor "[jq
## Installation

```bash
$ composer require invoicesharing/json-query-wrapper
$ composer require estahn/json-query-wrapper
```

## Usage
Expand Down Expand Up @@ -58,6 +58,31 @@ $jq->setDataProvider(new JsonQueryWrapper\DataProvider\File('test.json');
$jq->run('.Foo.Bar == "33"'); # Returns bool(true)
```

## Command Line Options

This enables passing additional command line options to `jq` to customize output and behavior. Currently, the following options are supported:

| Option | `jq` Option | Default? | Description |
| --- | --- | --- | --- |
| OPTION_EXIT_BASED_ON_OUTPUT | -e | no | set exit status based on whether output was successful |
| OPTION_INPUT_RAW | -R | no | don't parse input as JSON |
| OPTION_NULL_INPUT | -n | no | don't read any input at all |
| OPTION_OUTPUT_COLORIZE | -C | no | colorize output on shell |
| OPTION_OUTPUT_COMPACT | -c | no | compact output, don't pretty print |
| OPTION_OUTPUT_JOIN | -j | no | join each line of output, don't print newline |
| OPTION_OUTPUT_MONOCHROME | -M | yes | don't colorize output on the shell, print plainly |
| OPTION_OUTPUT_RAW | -r | no | write output directly, don't convert to JSON |
| OPTION_OUTPUT_SORT_KEYS | -S | no | sort keys in the output |

If not explicitly set, only the monochrome option will be set. More information about all these command line options can be found in the [jq manual](https://stedolan.github.io/jq/manual/#Invokingjq). To use command line options, just pass an array to the `JsonQueryFactory::create` or `JsonQueryFactory::createWith` methods:

```php
$options = [JsonQueryWrapper\CommandLineOption\CommandLineOption::OPTION_OUTPUT_JOIN, JsonQueryWrapper\CommandLineOption\CommandLineOption::OPTION_OUTPUT_SORT_KEYS];
$jq = JsonQueryWrapper\JsonQueryFactory::createWith('test.json', $options);
$jq->run('.Foo.Bar'); # string(33)
```


## Data Providers

A "Data Provider" provides the wrapper with the necessary data to read from. It's a common interface for several providers. All providers implement the `DataProviderInterface` which essentially returns a path to the file for `jq`.
Expand Down
47 changes: 47 additions & 0 deletions src/JsonQueryWrapper/CommandLineOption/CommandLineOption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace JsonQueryWrapper\CommandLineOption;

class CommandLineOption implements CommandLineOptionInterface
{
/**
* @param array
*/
protected $options;

public function __construct(array $options = [])
{
$this->options = [];
foreach ($options as $option) {
$this->addOption($option);
}
}

public function addOption(string $option): void
{
if (!in_array($option, $this->options) && static::isValidOption($option)) {
$this->options[] = $option;
}
}

public static function isValidOption(string $option): bool
{
$reflector = new \ReflectionClass(CommandLineOptionInterface::class);
$availableOptions = array_values($reflector->getConstants());
return in_array($option, $availableOptions);
}

public function getOptionsAsString(): ?string
{
return empty($this->options) ? null : '-' . implode('', $this->options);
}

public function removeOption(string $option): void
{
$optionIndex = array_keys($this->options, $option)[0] ?? null;
if ($optionIndex !== null) {
unset($this->options[$optionIndex]);
$this->options = array_values($this->options);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace JsonQueryWrapper\CommandLineOption;

interface CommandLineOptionInterface
{
const OPTION_EXIT_BASED_ON_OUTPUT = 'e';
const OPTION_INPUT_RAW = 'R';
const OPTION_NULL_INPUT = 'n';
const OPTION_OUTPUT_COLORIZE = 'C';
const OPTION_OUTPUT_COMPACT = 'c';
const OPTION_OUTPUT_JOIN = 'j';
const OPTION_OUTPUT_MONOCHROME = 'M';
const OPTION_OUTPUT_RAW = 'r';
const OPTION_OUTPUT_SORT_KEYS = 'S';

public function addOption(string $option): void;
public static function isValidOption(string $option): bool;
public function getOptionsAsString(): ?string;
public function removeOption(string $option): void;
}
29 changes: 12 additions & 17 deletions src/JsonQueryWrapper/DataTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,38 +29,33 @@ class DataTypeMapper
*/
public function map($value)
{
if ($value === 'true') {
return true;
}

if ($value === 'false') {
return false;
$boolean = filter_var($value, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE);
if ($boolean !== null) {
return $boolean;
}

if ($value === 'null') {
return;
return null;
}

// Map integers
if (preg_match('/^(\d+)$/', $value, $matches)) {
return (int) $matches[1];
if (is_numeric($value)) {
return $value + 0;
}

// Map floats
if (preg_match('/^(\d+\.\d+)$/', $value, $matches)) {
return (float) $matches[1];
// Map parser error
if (preg_match('/^parse error: (.*)$/', $value, $matches)) {
throw new DataTypeMapperException($matches[1]);
}

// Map strings
if (preg_match('/^"(.*)"$/', $value, $matches)) {
return $matches[1];
}

// Map parser error
if (preg_match('/^parse error: (.*)$/', $value, $matches)) {
throw new DataTypeMapperException($matches[1]);
$data = json_decode($value);
if ($data !== null) {
return $data;
}

return $value;
}
}
44 changes: 33 additions & 11 deletions src/JsonQueryWrapper/JsonQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,24 @@

namespace JsonQueryWrapper;

use JsonQueryWrapper\CommandLineOption\CommandLineOption;
use JsonQueryWrapper\CommandLineOption\CommandLineOptionInterface;
use JsonQueryWrapper\DataProvider\DataProviderInterface;
use JsonQueryWrapper\Exception\DataProviderMissingException;
use JsonQueryWrapper\Exception\DataTypeMapperException;
use JsonQueryWrapper\Process\ProcessFactoryInterface;
use Symfony\Component\Process\Exception\ProcessFailedException;

/**
* Class JsonQuery.
*/
class JsonQuery
{
/**
* @var CommandLineOptionInterface
*/
protected $commandLineOption;

/**
* @var ProcessFactoryInterface
*/
Expand Down Expand Up @@ -47,11 +56,17 @@ class JsonQuery
*
* @param ProcessFactoryInterface $processFactory
* @param DataTypeMapper $dataTypeMapper
* @param CommandLineOptionInterface|null $commandLineOption
*/
public function __construct(ProcessFactoryInterface $processFactory, DataTypeMapper $dataTypeMapper)
public function __construct(ProcessFactoryInterface $processFactory, DataTypeMapper $dataTypeMapper, ?CommandLineOptionInterface $commandLineOption = null)
{
$this->processFactory = $processFactory;
$this->mapper = $dataTypeMapper;
if ($commandLineOption) {
$this->commandLineOption = $commandLineOption;
} else {
$this->commandLineOption = new CommandLineOption([CommandLineOptionInterface::OPTION_OUTPUT_MONOCHROME]);
}
}

/**
Expand All @@ -60,24 +75,31 @@ public function __construct(ProcessFactoryInterface $processFactory, DataTypeMap
* @param string $filter
*
* @return mixed
* @throws DataProviderMissingException
* @throws Exception\DataTypeMapperException
* @throws DataProviderMissingException|\RuntimeException
*/
public function run($filter)
{
if (!$this->dataProvider instanceof DataProviderInterface) {
throw new DataProviderMissingException('A data provider such as file or text is missing.');
}

$command = [$this->cmd, '-M', $filter, $this->dataProvider->getPath()];

$process = $this->processFactory->build($command);

$process->run();

$result = trim($process->getOutput());
$options = $this->commandLineOption->getOptionsAsString();
if ($options !== null) {
$command = [$this->cmd, $options, $filter, $this->dataProvider->getPath()];
} else {
$command = [$this->cmd, $filter, $this->dataProvider->getPath()];
}

return $this->mapper->map($result);
try {
$process = $this->processFactory->build($command);
$process->mustRun();
$result = trim($process->getOutput());
return $this->mapper->map($result);
} catch (ProcessFailedException $processFailedException) {
throw new \RuntimeException(trim($process->getErrorOutput()), $processFailedException->getProcess()->getExitCode(), $processFailedException);
} catch (DataTypeMapperException $dataTypeMapperException) {
throw new \RuntimeException($dataTypeMapperException->getMessage(), $dataTypeMapperException->getCode(), $dataTypeMapperException);
}
}

/**
Expand Down
14 changes: 10 additions & 4 deletions src/JsonQueryWrapper/JsonQueryFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,40 @@

namespace JsonQueryWrapper;

use JsonQueryWrapper\CommandLineOption\CommandLineOption;
use JsonQueryWrapper\DataProvider\File;
use JsonQueryWrapper\DataProvider\Text;
use JsonQueryWrapper\Process\ProcessFactory;

class JsonQueryFactory
{
const DEFAULT_OPTIONS = [CommandLineOption::OPTION_OUTPUT_MONOCHROME];

/**
* Creates a JsonQuery object without data provider.
*
* @param array $options command line options
*
* @return JsonQuery
*/
public static function create()
public static function create($options = self::DEFAULT_OPTIONS)
{
return new JsonQuery(new ProcessFactory(), new DataTypeMapper());
return new JsonQuery(new ProcessFactory(), new DataTypeMapper(), new CommandLineOption($options));
}

/**
* Creates a JsonQuery object with data provider.
*
* @param string $filenameOrText A path to a json file or json text
* @param array $options command line options
*
* @return JsonQuery
*/
public static function createWith($filenameOrText)
public static function createWith($filenameOrText, $options = self::DEFAULT_OPTIONS)
{
$provider = file_exists($filenameOrText) ? new File($filenameOrText) : new Text($filenameOrText);

$jq = new JsonQuery(new ProcessFactory(), new DataTypeMapper());
$jq = new JsonQuery(new ProcessFactory(), new DataTypeMapper(), new CommandLineOption($options));
$jq->setDataProvider($provider);

return $jq;
Expand Down
43 changes: 43 additions & 0 deletions tests/JsonQueryWrapper/CommandLineOptionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace JsonQueryWrapper\CommandLineOption;

use PHPUnit\Framework\TestCase;

class CommandLineOptionTest extends TestCase
{
public function testEmptyCommandLineOptions()
{
$options = new CommandLineOption();
$this->assertNull($options->getOptionsAsString());
}

public function testAddingCommandLineOptions()
{
$options = new CommandLineOption();
$options->addOption(CommandLineOption::OPTION_OUTPUT_JOIN);
$options->addOption(CommandLineOption::OPTION_OUTPUT_SORT_KEYS);
$this->assertEquals($options->getOptionsAsString(), '-jS');
}

public function testAddingDuplicateCommandLineOptions()
{
$options = new CommandLineOption();
$options->addOption(CommandLineOption::OPTION_OUTPUT_JOIN);
$options->addOption(CommandLineOption::OPTION_OUTPUT_SORT_KEYS);
$this->assertEquals($options->getOptionsAsString(), '-jS');
$options->addOption(CommandLineOption::OPTION_OUTPUT_JOIN);
$options->addOption(CommandLineOption::OPTION_OUTPUT_SORT_KEYS);
$this->assertEquals($options->getOptionsAsString(), '-jS');
}

public function testRemovingCommandLineOptions()
{
$options = new CommandLineOption();
$options->addOption(CommandLineOption::OPTION_OUTPUT_JOIN);
$options->addOption(CommandLineOption::OPTION_OUTPUT_SORT_KEYS);
$this->assertEquals($options->getOptionsAsString(), '-jS');
$options->removeOption(CommandLineOption::OPTION_OUTPUT_SORT_KEYS);
$this->assertEquals($options->getOptionsAsString(), '-j');
}
}
6 changes: 2 additions & 4 deletions tests/JsonQueryWrapper/DataTypeMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,8 @@ public function testNull()

public function testJson()
{
$this->markTestIncomplete('Need to decide whether to convert JSON data');

$json = ['Foo' => ['Bar' => 33]];
$this->assertEquals('Foo', $this->mapper->map(json_encode($json)));
$json = (object)['Foo' => (object)['Bar' => 33]];
$this->assertEquals($json, $this->mapper->map(json_encode($json)));
}

public function testGarbage()
Expand Down
4 changes: 3 additions & 1 deletion tests/JsonQueryWrapper/JsonQueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public function testCmdChange()
$dataProvider = new Text(json_encode($json));

$process = $this->createMock(Process::class);
$process
->method('mustRun');
$process
->method('run');

Expand All @@ -49,7 +51,7 @@ public function testCmdChange()
);

$sut->setDataProvider($dataProvider);
static::assertEquals(
$this->assertEquals(
$expected,
$sut->run('.name')
);
Expand Down