Skip to content

Commit

Permalink
[NAVAND-1714] Upgrade services-cli (#1583)
Browse files Browse the repository at this point in the history
* handle string input:
  -j "example json file"
* exit with code 1 when failed verification
* exit also with code 1 on back conversion:
  -c
* pretty print result:
  -p
* updated tests
  • Loading branch information
holuss authored Jun 14, 2024
1 parent 9bd9b7f commit be2ba14
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 38 deletions.
3 changes: 3 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ commands:
- run:
name: Build command line interface
command: make build-cli
- store_artifacts:
path: services-cli/build/libs/services-cli.jar
destination: services-cli.jar

run-tests:
steps:
Expand Down
2 changes: 1 addition & 1 deletion services-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ and we can help. Also, pull requests are always welcome!
From the command line
1. cd mapbox-java
1. Build with ./gradlew shadowJar, or make build-cli
1. Run with java -jar services-cli/build/libs/services-cli-all.jar
1. Run with java -jar services-cli/build/libs/services-cli.jar

From Android Studio
1. Open mapbox-java with Android Studio
Expand Down
109 changes: 91 additions & 18 deletions services-cli/src/main/kotlin/com.mapbox.services.cli/MapboxJavaCli.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.mapbox.services.cli

import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.mapbox.services.cli.validator.DirectionsResponseValidator
import com.mapbox.services.cli.validator.ValidatorInput
import com.mapbox.services.cli.validator.ValidatorResult
import org.apache.commons.cli.CommandLine
import org.apache.commons.cli.DefaultParser
import org.apache.commons.cli.HelpFormatter
Expand All @@ -14,22 +16,58 @@ import org.apache.commons.cli.ParseException
*/
object MapboxJavaCli {

private const val COMMAND_FILE_INPUT = "f"
private const val COMMAND_HELP = "h"
private const val COMMAND_FILE_INPUT = "f"
private const val COMMAND_JSON_INPUT = "j"
private const val COMMAND_PRETTY_PRINT = "p"
private const val COMMAND_FAIL_ON_CONVERT_BACK = "c"

@JvmStatic
fun main(args: Array<String>) {
val options = Options()
.addOption(Option.builder(COMMAND_HELP)
.longOpt("help")
.desc("Shows this help message")
.build())
.addOption(Option.builder(COMMAND_FILE_INPUT)
.longOpt("file")
.hasArg(true)
.desc("Path to a json file or directory")
.required()
.build())
.addOption(
Option.builder(COMMAND_HELP)
.longOpt("help")
.desc("Shows this help message")
.build()
)
.addOption(
Option.builder(COMMAND_FILE_INPUT)
.longOpt("file")
.hasArg(true)
.desc("Path to a json file or directory. If directory is provided " +
"all files in it will be processed.")
.required(false)
.build()
)
.addOption(
Option.builder(COMMAND_JSON_INPUT)
.longOpt("json")
.hasArg(true)
.desc("String containing the json. Instead of providing files it " +
"is possible to relay the json directly.")
.required(false)
.build()
)
.addOption(
Option.builder(COMMAND_PRETTY_PRINT)
.longOpt("pretty")
.desc("Pretty printing of results. Colored and indented JSON " +
"output. It consist of many characters which are not visible in the " +
"console - that why it might be hard to parse such JSON. If you want " +
"to parse the result it's recommended not use this option.")
.required(false)
.build()
)
.addOption(
Option.builder(COMMAND_FAIL_ON_CONVERT_BACK)
.longOpt("convert-fail")
.desc("Exit also with code 1 if the conversion back to JSON fails. " +
"This is useful to check if mapbox-java produces the same JSON file " +
"that was provided as an input from its internal structures.")
.required(false)
.build()
)

try {
val commandLine = DefaultParser().parse(options, args)
Expand All @@ -41,18 +79,53 @@ object MapboxJavaCli {
}

private fun parseCommands(commandLine: CommandLine, options: Options) {
if (commandLine.hasOption(COMMAND_HELP)) {
if (commandLine.hasOption(COMMAND_HELP) || commandLine.options.isEmpty()) {
printHelp(options)
return
}

val fileInput = commandLine.getOptionValue(COMMAND_FILE_INPUT)
val directionsResponseValidator = DirectionsResponseValidator()
val results = directionsResponseValidator.parse(fileInput)
print(Gson().toJson(results))
val results = mutableListOf<ValidatorResult>()

when {
commandLine.hasOption(COMMAND_JSON_INPUT) -> {
val jsonInput = commandLine.getOptionValue(COMMAND_JSON_INPUT)
results.add(directionsResponseValidator.parseJson(jsonInput))
}

commandLine.hasOption(COMMAND_FILE_INPUT) -> {
val fileInput = commandLine.getOptionValue(COMMAND_FILE_INPUT)
results.addAll(directionsResponseValidator.parseFile(fileInput))
}
}

val failure = !results.all { it.success } ||
(commandLine.hasOption(COMMAND_FAIL_ON_CONVERT_BACK) && !results.all { it.convertsBack })

printResult(results, failure, commandLine.hasOption(COMMAND_PRETTY_PRINT))

if (failure) {
System.exit(1)
}
}

private fun printHelp(options: Options) {
val syntax = "java -jar services-cli/build/libs/services-cli-all.jar"
HelpFormatter().printHelp(syntax, options)
val syntax = "java -jar services-cli/build/libs/services-cli.jar <option>"
val header = "\nMapbox Java CLI. Validates DirectionsApi responses." +
"CHeck correctness of the provided JSON (files or strings). " +
"Process with code 1 on parse errors or (optionally) on conversion " +
"back. Exits with code 0 on success. Returns JSON formatted result."
HelpFormatter().printHelp(syntax, header, options, "")
}

private fun printResult(
results: List<ValidatorResult>, failure: Boolean, prettyPrint: Boolean
) {
var message = GsonBuilder().also { if (prettyPrint) it.setPrettyPrinting() }
.registerTypeAdapterFactory(ValidatorInput.gsonAdapter()).create().toJson(results)
if (prettyPrint) {
message = (if (failure) "\u001B[31m" else "\u001B[32m") + "$message\u001B[0m\n"
}
print(message)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,50 @@ class DirectionsResponseValidator {
* @param filePath path to the json file or directory
* @return results for all the files
*/
fun parse(filePath: String): List<ValidatorResult> {
fun parseFile(filePath: String): List<ValidatorResult> {
val inputFile = File(filePath)

val results = mutableListOf<ValidatorResult>()
inputFile.forEachFile { file ->
val result = validateJson(file)
val result = validateFile(file, ValidatorInput.File(file.name))
results.add(result)
}
return results
}

/**
* @param json JSON formatted string
* @return results parsed from the JSON
*/
fun parseJson(json: String): ValidatorResult = validateJson(json, ValidatorInput.Json)

private fun File.forEachFile(function: (File) -> Unit) = walk()
.filter { !it.isDirectory }
.forEach(function)

private fun validateJson(file: File): ValidatorResult {
private fun validateFile(file: File, input: ValidatorInput): ValidatorResult {
val json = file.readText(UTF_8)
return validateJson(json, input)
}

private fun validateJson(json: String, input: ValidatorInput): ValidatorResult {
return try {
val directionsResponse = DirectionsResponse.fromJson(json)
val toJson = directionsResponse.toJson()
val convertsBack = json == toJson
ValidatorResult(
filename = file.name,
input = input,
success = true,
convertsBack = convertsBack
)
} catch (throwable: Throwable) {
ValidatorResult(
filename = file.name,
input = input,
success = false,
convertsBack = false,
throwable = throwable
error = throwable.message
)
}
}

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
package com.mapbox.services.cli.validator

import com.google.gson.annotations.SerializedName
import com.mapbox.geojson.internal.typeadapters.RuntimeTypeAdapterFactory

data class ValidatorResult(
val filename: String,
val input: ValidatorInput,
val success: Boolean,
@SerializedName("converts_back")
val convertsBack: Boolean,
val throwable: Throwable? = null
val error: String? = null
)


sealed class ValidatorInput {
data class File(val name: String) : ValidatorInput()
object Json : ValidatorInput()

companion object {
fun gsonAdapter(): RuntimeTypeAdapterFactory<ValidatorInput> {
return RuntimeTypeAdapterFactory
.of<ValidatorInput>(ValidatorInput::class.java, "type")
.registerSubtype(
File::class.java,
File::class.simpleName
)
.registerSubtype(
Json::class.java,
Json::class.simpleName
)
}
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.mapbox.services.cli.validator

import com.google.common.truth.Truth.assertThat
import org.junit.Test
import java.io.File
import kotlin.test.assertTrue

class DirectionsResponseValidatorTest {
Expand All @@ -12,30 +13,55 @@ class DirectionsResponseValidatorTest {
fun `should successfully read file with DirectionsResponse json`() {
val testFile = "./src/test/resources/directions_v5.json"

val results = directionsResponseValidator.parse(testFile)
val results = directionsResponseValidator.parseFile(testFile)

assertTrue(results[0].success)
assertThat(results[0].filename).isEqualTo("directions_v5.json")
assertThat(results[0].throwable).isNull()
assertThat(results[0].error).isNull()
}

@Test
fun `should successfully read string with correct DirectionsResponse json`() {
val testText = File("./src/test/resources/directions_v5.json").readText(Charsets.UTF_8)

val result = directionsResponseValidator.parseJson(testText)

assertTrue(result.success)
assertThat(result.input).isEqualTo(ValidatorInput.Json)
assertThat(result.error).isNull()
}

@Test
fun `should detect if file is not DirectionsResponse json`() {
val testFile = "./src/test/resources/geojson_feature.json"

val results = directionsResponseValidator.parse(testFile)
val results = directionsResponseValidator.parseFile(testFile)

assertThat(results[0].success).isFalse()
assertThat(results[0].filename).isEqualTo("geojson_feature.json")
assertThat(results[0].throwable).isNotNull()
assertThat(results[0].error).isNotNull()
assertThat(results[0].convertsBack).isFalse()
}

@Test
fun `should detect if provided string is not DirectionsResponse formatted json`() {
val testText = File("./src/test/resources/geojson_feature.json")
.readText(Charsets.UTF_8)


val result = directionsResponseValidator.parseJson(testText)

assertThat(result.success).isFalse()
assertThat(result.input).isEqualTo(ValidatorInput.Json)
assertThat(result.error).isNotNull()
assertThat(result.convertsBack).isFalse()
}

@Test(expected = Exception::class)
fun `should crash when json does not exist`() {
fun `should crash when json file does not exist`() {
val testFile = "not a real file path"

val results = directionsResponseValidator.parse(testFile)
val results = directionsResponseValidator.parseFile(testFile)

assertTrue(results[0].success)
}
Expand All @@ -44,8 +70,10 @@ class DirectionsResponseValidatorTest {
fun `should parse every file in the directory`() {
val testFile = "./src/test/resources"

val results = directionsResponseValidator.parse(testFile)
val results = directionsResponseValidator.parseFile(testFile)

assertThat(results.size).isGreaterThan(1)
}

private val ValidatorResult.filename: String? get() = (input as? ValidatorInput.File)?.name
}
Loading

0 comments on commit be2ba14

Please sign in to comment.