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

Introduce jsonmerge-cli #24

Merged
merged 11 commits into from
Oct 27, 2022
1 change: 1 addition & 0 deletions jsonmerge-cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target/
60 changes: 60 additions & 0 deletions jsonmerge-cli/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<!--
Copyright 2022 obvj.net

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>net.obvj</groupId>
<artifactId>jsonmerge</artifactId>
<version>1.1.1-SNAPSHOT</version>
</parent>

<artifactId>jsonmerge-cli</artifactId>
<name>JSON Merge CLI</name>
<description>
CLI tool for merging JSON files
</description>

<dependencies>

<dependency>
<groupId>net.obvj</groupId>
<artifactId>jsonmerge-core</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.1</version>
</dependency>

<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.6.3</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.33</version>
</dependency>

</dependencies>

</project>
oswaldobapvicjr marked this conversation as resolved.
Show resolved Hide resolved
169 changes: 169 additions & 0 deletions jsonmerge-cli/src/main/java/net/obvj/jsonmerge/cli/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* Copyright 2022 obvj.net
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.obvj.jsonmerge.cli;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Callable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;

import net.obvj.jsonmerge.JsonMergeOption;
import net.obvj.jsonmerge.JsonMerger;
import net.obvj.jsonmerge.provider.JsonProvider;
import net.obvj.jsonmerge.provider.JsonProviderFactory;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;

/**
* A command line tool to merge JSON files.
* <p>
* Usage:
*
* <pre>
* {@code Main [-hp] -t <target> [-d <exp=key>]... <FILE1> <FILE2>}
* </pre>
*
* @author oswaldo.bapvic.jr
* @since 1.2.0
*/
@Command(name = "jsonmerge-cli-1.2.0.jar",
separator = " ",
usageHelpWidth = 85,
parameterListHeading = "\nParameters:\n\n",
optionListHeading = "\nOptions:\n\n")
public class Main implements Callable<Integer>
{

@Parameters(index = "0",
paramLabel = "<FILE1>",
description = "The first file to merge")
private Path file1;

@Parameters(index = "1",
paramLabel = "<FILE2>",
description = "The second file to merge")
private Path file2;

@Option(names = { "-t", "--target" },
description = "The target file name (default: result.json)",
defaultValue = "result.json",
required = true)
private Path target;

@Option(names = { "-d", "--distinct" },
paramLabel = "<exp=key>",
description = { "Defines one or more distinct keys inside a child path",
"For example: -d $.agents=name",
" -d $.files=id,version" })
private Map<String, String> distinctKeys = Collections.emptyMap();

@Option(names = { "-p", "--pretty" },
description = "Generates a well-formatted result file")
private boolean prettyPrinting = false;

@Option(names = { "-h", "--help" },
usageHelp = true,
description = "Displays a help message")
private boolean helpRequested = false;

private JsonProvider<JsonObject> provider = JsonProviderFactory.instance()
.getByType(JsonObject.class);

private GsonBuilder gsonBuilder = new GsonBuilder();

private Logger log = LoggerFactory.getLogger(Main.class);

/**
* Executes the CLI logic.
*
* @return the exit code to be handled by the CLI framework
*/
@Override
public Integer call() throws Exception
{
String result = parseAndMerge();

log.info("Generating output file {} ...", target);
Files.write(target, result.getBytes());

log.info("Success");
return 0;
}

String parseAndMerge() throws IOException
{
if (prettyPrinting) gsonBuilder.setPrettyPrinting();

log.info("Parsing {} ...", file1);
JsonObject json1 = provider.parse(Files.newInputStream(file1));

log.info("Parsing {} ...", file2);
JsonObject json2 = provider.parse(Files.newInputStream(file2));

JsonMergeOption[] mergeOptions = parseJsonMergeOptions();
JsonObject result = new JsonMerger<>(JsonObject.class)
.merge(json1, json2, mergeOptions);

return toString(result);
}

JsonMergeOption[] parseJsonMergeOptions()
{
return distinctKeys.entrySet().stream()
.map(this::parseJsonMergeOption)
.toArray(JsonMergeOption[]::new);
}

private JsonMergeOption parseJsonMergeOption(Entry<String, String> entry)
{
JsonMergeOption mergeOption = JsonMergeOption.onPath(entry.getKey())
.findObjectsIdentifiedBy(split(entry.getValue()))
.thenDoADeepMerge();
log.info("{}", mergeOption);
return mergeOption;
}

private String[] split(String string)
{
return string.split(",");
}

private String toString(JsonObject json)
{
return gsonBuilder.create().toJson(json);
}

/**
* @param args the command line arguments to parse
*/
public static void main(String[] args)
{
System.exit(new CommandLine(new Main()).execute(args));
}

}
132 changes: 132 additions & 0 deletions jsonmerge-cli/src/test/java/net/obvj/jsonmerge/cli/MainTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* Copyright 2022 obvj.net
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.obvj.jsonmerge.cli;

import static net.obvj.jsonmerge.JsonMergeOption.onPath;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;

import org.junit.jupiter.api.Test;

import net.obvj.jsonmerge.JsonMergeOption;
import picocli.CommandLine;

/**
* Unit tests for the {@link Main} class.
*
* @author oswaldo.bapvic.jr
* @since 1.2.0
*/
class MainTest
{
private static final String TARGET_FILE = "target/result.json";

// Program arguments
private static final String ARG_FILE1 = "src/test/resources/file1.json";
private static final String ARG_FILE2 = "src/test/resources/file2.json";
private static final String ARG_D_PATH1_KEY1 = "-dpath1=key1";
private static final String ARG_D_PATH1_KEY1_KEY2 = "-dpath1=key1,key2";
private static final String ARG_D_PATH2_KEY3 = "-dpath2=key3";
private static final String ARG_PRETTY = "-p";
private static final String ARG_TARGET_FILE = "-t " + TARGET_FILE;


// Expected objects
private static final JsonMergeOption MERGE_OPTION_PATH1_KEY1 = onPath("path1")
.findObjectsIdentifiedBy("key1").thenDoADeepMerge();
private static final JsonMergeOption MERGE_OPTION_PATH1_KEY1_KEY2 = onPath("path1")
.findObjectsIdentifiedBy("key1", "key2").thenDoADeepMerge();
private static final JsonMergeOption MERGE_OPTION_PATH2_KEY3 = onPath("path2")
.findObjectsIdentifiedBy("key3").thenDoADeepMerge();
private static final String MERGE_JSON1_JSON2_COMPACT = "{\"keyA\":\"valueA\",\"keyB\":\"valueB\"}";
private static final List<String> MERGE_JSON1_JSON2_PRETTY = Arrays.asList("{",
" \"keyA\": \"valueA\",", " \"keyB\": \"valueB\"", "}");


// Test subject
private Main main = new Main();
private CommandLine commandLine = new CommandLine(main);

@Test
void parseJsonMergeOptions_oneKey_success()
{
commandLine.parseArgs(ARG_D_PATH1_KEY1, ARG_FILE1, ARG_FILE2);
assertThat(main.parseJsonMergeOptions(),
equalTo(new JsonMergeOption[] { MERGE_OPTION_PATH1_KEY1 }));
}

@Test
void parseJsonMergeOptions_twoKeys_success()
{
commandLine.parseArgs(ARG_D_PATH1_KEY1_KEY2, ARG_FILE1, ARG_FILE2);
assertThat(main.parseJsonMergeOptions(),
equalTo(new JsonMergeOption[] { MERGE_OPTION_PATH1_KEY1_KEY2 }));
}

@Test
void parseJsonMergeOptions_twoArguments_success()
{
commandLine.parseArgs(ARG_D_PATH1_KEY1_KEY2, ARG_D_PATH2_KEY3, ARG_FILE1, ARG_FILE2);
assertThat(main.parseJsonMergeOptions(), equalTo(
new JsonMergeOption[] { MERGE_OPTION_PATH1_KEY1_KEY2, MERGE_OPTION_PATH2_KEY3 }));
}

@Test
void parseAndMerge_validFiles_compactResult() throws IOException
{
commandLine.parseArgs(ARG_FILE1, ARG_FILE2);
assertThat(main.parseAndMerge(), equalTo(MERGE_JSON1_JSON2_COMPACT));
}

@Test
void parseAndMerge_validFilesAndPrettyOption_prettyResult() throws IOException
{
commandLine.parseArgs(ARG_PRETTY, ARG_FILE1, ARG_FILE2);
assertAllLines(MERGE_JSON1_JSON2_PRETTY, main.parseAndMerge());
}

private static void assertAllLines(List<String> expectedLines, String actual)
{
String[] actualLines = actual.split("\n");
for (int i = 0; i < actualLines.length; i++)
{
assertThat(actualLines[i], equalTo(expectedLines.get(i)));
}
}

@Test
void execute_fileNotFound_errorCode()
{
assertThat(commandLine.execute("nonExistingFile.json", ARG_FILE2), equalTo(1));
}

@Test
void execute_validFiles_targetFileGeneratedSuccessfully() throws IOException
{
assertThat(commandLine.execute(ARG_TARGET_FILE, ARG_FILE1, ARG_FILE2), equalTo(0));
Path targetFile = FileSystems.getDefault().getPath(TARGET_FILE);
assertThat(Files.readAllBytes(targetFile), equalTo(MERGE_JSON1_JSON2_COMPACT.getBytes()));
}

}
3 changes: 3 additions & 0 deletions jsonmerge-cli/src/test/resources/file1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"keyA": "valueA"
}
3 changes: 3 additions & 0 deletions jsonmerge-cli/src/test/resources/file2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"keyB": "valueB"
}
7 changes: 7 additions & 0 deletions jsonmerge-core/pom.xml
Original file line number Diff line number Diff line change
@@ -88,5 +88,12 @@
</exclusions>
</dependency>

<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<scope>test</scope>
</dependency>

</dependencies>
</project>
Loading