diff --git a/JsonSchemaValidator.java b/JsonSchemaValidator.java new file mode 100644 index 0000000..edccedc --- /dev/null +++ b/JsonSchemaValidator.java @@ -0,0 +1,135 @@ +import org.everit.json.schema.Schema; +import org.everit.json.schema.ValidationException; +import org.everit.json.schema.loader.SchemaLoader; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +public class JsonSchemaValidator { + + private static final String DEFAULT_SCHEMA_PATH = "schema.json"; + private static final String SCHEMA_ENV_VAR = "JSON_SCHEMA_PATH"; + public static void main(String[] args) { + if (args.length != 1) { + System.out.println("Usage: java JsonSchemaValidator "); + System.exit(1); + } + + String jsonPath = args[0]; + String schemaPath = System.getenv(SCHEMA_ENV_VAR); + if (schemaPath == null || schemaPath.isEmpty()) { + schemaPath = DEFAULT_SCHEMA_PATH; + System.out.println("Warning: JSON_SCHEMA_PATH not set. Using default schema path: " + DEFAULT_SCHEMA_PATH); + } + + try { + String jsonContent = new String(Files.readAllBytes(Paths.get(jsonPath))); + JSONObject jsonSchema = new JSONObject(new JSONTokener(new FileInputStream(schemaPath))); + JSONObject jsonSubject = new JSONObject(jsonContent); + + Schema schema = SchemaLoader.load(jsonSchema); + schema.validate(jsonSubject); + + System.out.println("Validation successful! The JSON is valid against the schema."); + + } catch (ValidationException ve) { + System.out.println("JSON validation failed. Errors:"); + printValidationErrors(ve, jsonPath); + } catch (JSONException je) { + System.out.println("JSON parsing error:"); + printJSONParsingError(je, jsonPath); + } catch (IOException e) { + System.out.println("Error reading files: " + e.getMessage()); + } + } + + private static void printValidationErrors(ValidationException ve, String jsonPath) { + List allMessages = ve.getAllMessages(); + for (int i = 0; i < allMessages.size(); i++) { + System.out.printf("%d. %s%n", i + 1, allMessages.get(i)); + } + System.out.println("\nDetailed error information:"); + printValidationErrorDetails(ve, jsonPath, 0); + } + + private static void printValidationErrorDetails(ValidationException ve, String jsonPath, int depth) { + String indent = " ".repeat(depth); + System.out.printf("%sError: %s%n", indent, ve.getMessage()); + System.out.printf("%sJSON Path: %s%n", indent, ve.getPointerToViolation()); + + try { + List lines = Files.readAllLines(Paths.get(jsonPath)); + String errorPath = ve.getPointerToViolation(); + int lineNumber = findLineNumber(lines, errorPath); + if (lineNumber != -1) { + System.out.printf("%sLine number: %d%n", indent, lineNumber); + System.out.printf("%sLine content: %s%n", indent, lines.get(lineNumber - 1).trim()); + } + } catch (IOException e) { + System.out.printf("%sUnable to retrieve line information: %s%n", indent, e.getMessage()); + } + + List causingExceptions = ve.getCausingExceptions(); + if (!causingExceptions.isEmpty()) { + System.out.printf("%sNested errors:%n", indent); + for (ValidationException cause : causingExceptions) { + printValidationErrorDetails(cause, jsonPath, depth + 1); + } + } + } + + private static void printJSONParsingError(JSONException je, String jsonPath) { + System.out.println(je.getMessage()); + + try { + List lines = Files.readAllLines(Paths.get(jsonPath)); + if (je.getMessage().contains("at character")) { + int charPosition = Integer.parseInt(je.getMessage().replaceAll(".*at character (\\d+).*", "$1")); + int lineNumber = 1; + int currentPosition = 0; + + for (String line : lines) { + if (currentPosition + line.length() + 1 > charPosition) { + System.out.printf("Line number: %d%n", lineNumber); + System.out.printf("Line content: %s%n", line.trim()); + System.out.printf("Error position: %s^%n", " ".repeat(charPosition - currentPosition - 1)); + break; + } + currentPosition += line.length() + 1; // +1 for newline + lineNumber++; + } + } + } catch (IOException e) { + System.out.println("Unable to retrieve line information: " + e.getMessage()); + } + } + + private static int findLineNumber(List lines, String jsonPath) { + String[] pathParts = jsonPath.split("/"); + StringBuilder currentPath = new StringBuilder(); + int nestingLevel = 0; + + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i).trim(); + if (line.contains(":")) { + String key = line.split(":")[0].trim().replace("\"", ""); + if (nestingLevel < pathParts.length && key.equals(pathParts[nestingLevel])) { + currentPath.append("/").append(key); + nestingLevel++; + if (currentPath.toString().equals(jsonPath)) { + return i + 1; // +1 because line numbers start at 1 + } + } + } + if (line.contains("{")) nestingLevel++; + if (line.contains("}")) nestingLevel--; + } + return -1; // Path not found + } +} diff --git a/README.md b/README.md index 58a409e..5ec6dde 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,27 @@ public class Example { Tax provider capabilities for new tax providers will be validated against a constantly updating JSON Schema, to validate the correctness and completeness of configurations. JSON Schema can be referenced below. - [TaxProviderCapabilities JSONSchema](spec/capabilities/tax-provider.schema.json) +Prerequisites: +Use Java8 or higher version. + +1. Clone repository in local +```shell + git clone git@github.com:chargebee/cb-provider-spi.git +``` + +2. Navigate to the repository +```shell + cd cb-provider-spi +``` +3. Run the script to perform json schema validation: +```shell + sh json_schema_validation.sh +``` +Example: +```shell + sh json_schema_validation.sh spec/capabilities/tax-provider.file.json +``` + ## Steps to follow release diff --git a/build.gradle b/build.gradle index ab5f15c..60b654a 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,14 @@ group build_group version build_version sourceSets { - main.resources.srcDirs = ['spec/capabilities'] + main { + java { + srcDirs = ['src/main/java', '.'] + } + resources { + srcDirs = ['src/main/resources', 'spec/capabilities'] + } + } } repositories { @@ -21,8 +28,30 @@ repositories { } java { - sourceCompatibility = "17" - targetCompatibility = "17" + sourceCompatibility = "11" + targetCompatibility = "11" +} + +// JSON Schema Validator JAR configuration +task jsonSchemaValidatorJar(type: Jar) { + archiveBaseName = 'json-schema-validator' + archiveVersion = '1.0' + manifest { + attributes 'Main-Class': 'JsonSchemaValidator' + } + from(sourceSets.main.output) { + include 'JsonSchemaValidator.class' + include 'spec/capabilities/**' + } + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +task buildJsonSchemaValidator { + dependsOn jsonSchemaValidatorJar } def loadSpecConfig() { @@ -119,4 +148,6 @@ dependencies { testImplementation 'org.mockito:mockito-core:5.6.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' + // JSON Schema validation dependencies + implementation 'com.github.erosb:everit-json-schema:1.14.2' } \ No newline at end of file diff --git a/json_schema_validation.sh b/json_schema_validation.sh new file mode 100755 index 0000000..b0d238b --- /dev/null +++ b/json_schema_validation.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +# Make this script executable +chmod +x "$0" + +# Check if Gradle wrapper is available +if ! command -v ./gradlew &> /dev/null +then + echo "Gradle wrapper not found. Please ensure you're in the correct directory." + exit 1 +fi + +# Check if a JSON file is provided +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Create a temporary directory to store the contents of the generated folder +TEMP_DIR=$(mktemp -d) + +# If the generated folder exists, copy its contents to the temporary directory +if [ -d "generated" ]; then + cp -R generated/* "$TEMP_DIR" 2>/dev/null +fi + +# Delete the generated folder +rm -rf generated 2>/dev/null + +# Automatically detect the schema file +SCHEMA_FILE=$(find . -name "*schema.json" | head -n 1) + +if [ -z "$SCHEMA_FILE" ]; then + echo "Error: Could not find a schema file in the current directory or its subdirectories." + rm -rf "$TEMP_DIR" + exit 1 +fi + +# Export the schema path as an environment variable +export JSON_SCHEMA_PATH="$SCHEMA_FILE" + +echo "Using schema file: $JSON_SCHEMA_PATH" + +# Clean and build the JSON Schema Validator +./gradlew clean buildJsonSchemaValidator + +# Find the JAR file +JAR_FILE=$(find build/libs -name "json-schema-validator-1.0.jar" | head -n 1) + +if [ -z "$JAR_FILE" ]; then + echo "Error: Could not find the JSON Schema Validator JAR file. Build may have failed." + rm -rf "$TEMP_DIR" + exit 1 +fi + +# Run the Java application using the generated JAR file +java -jar "$JAR_FILE" "$1" + +# Check the exit status +VALIDATION_STATUS=$? +if [ $VALIDATION_STATUS -eq 0 ]; then + echo "Validation completed successfully." +else + echo "Validation failed. Please check the output above for details." +fi + +# Recreate the generated folder and restore its contents +mkdir -p generated +cp -R "$TEMP_DIR"/* generated 2>/dev/null + +# Clean up the temporary directory +rm -rf "$TEMP_DIR" + +exit $VALIDATION_STATUS \ No newline at end of file diff --git a/spec/capabilities/tax-provider.file.json b/spec/capabilities/tax-provider.file.json new file mode 100644 index 0000000..2db0357 --- /dev/null +++ b/spec/capabilities/tax-provider.file.json @@ -0,0 +1,186 @@ +{ + "prod":{ + "api_configuration":{ + "api_base_url":"https://api.anrok.com/integrations/chargebee/api/v1", + "credential_configuration":[ + { + "id":"api_key", + "is_required":true, + "is_sensitive":true, + "name":"Anrok API Key", + "type":"text" + } + ] + }, + "capabilities":{ + "can_have_customer_identifiers":true, + "can_have_product_identifiers":false, + "can_support_currency_inclusive_of_taxes":true, + "can_sync_credit_notes":true, + "can_sync_invoices":true, + "can_validate_shipping_address":false, + "credit_note_sync_capabilities":{ + "applicable_sync_types":[ + "SYNC_ALL" + ], + "can_commit":false, + "can_delete":false, + "can_void":true, + "is_sync_supported":true, + "supported_countries":[ + "US", + "EU", + "GB", "ALL" + ] + }, + "invoice_sync_capabilities":{ + "applicable_sync_types":[ + "SYNC_ALL" + ], + "can_commit":false, + "can_delete":false, + "can_void":true, + "is_sync_supported":true, + "supported_countries":[ + "US", + "EU", + "GB", "ALL" + ] + }, + "is_consistent_pricing_supported":false, + "supported_countries":[ + "US", + "EU", + "GB", "ALL" + ], + "supported_currencies":[ + "US_DOLLARS" + ], + "tax_calculation_capabilities":{ + "accept_invalid_tax_reg_numbers":false, + "supportedNumberOfLineItems":1200 + } + }, + "customer_identifiers":[ + { + "external_id": "additionalTaxRegistrationNumber", + "display_name": "Additional Tax Registration Number", + "is_mandatory": false, + "field_type": "text" + } + ], + "identity_configuration":{ + "consent_policy_url":" https://www.anrok.com/privacy-terms", + "display_name":"Anrok", + "documentation_url":"https://www.chargebee.com/docs/2.0/anrok.html", + "id":"anrok", + "logo_url":"https://app.anrok.com/images/anrok-chargebee-app-icon.png", + "primary_description":[ + "Anrok unifies sales tax monitoring, calculation and remittance across your financial stack." + ], + "privacy_policy_url":" https://www.anrok.com/privacy-terms", + "secondary_description":[ + "Future-proof your Internet revenue." + ], + "signup_url":"https://anrok.com/contact/request-demo", + "support_email":"support@anrok.com", + "terms_of_service_url":"https://www.anrok.com/privacy-terms" + }, + "product_identifiers":[ + ], + "supported_number_of_line_items":1000, + "version":"1.0.0" + }, + "sandbox":{ + "api_configuration":{ + "api_base_url":"https://api.anrok.com/integrations/chargebee/api/v1", + "credential_configuration":[ + { + "id":"api_key", + "is_required":true, + "is_sensitive":true, + "name":"Anrok API Key", + "type":"text" + } + ] + }, + "capabilities":{ + "can_have_customer_identifiers":true, + "can_have_product_identifiers":false, + "can_support_currency_inclusive_of_taxes":true, + "can_sync_credit_notes":true, + "can_sync_invoices":true, + "can_validate_shipping_address":false, + "credit_note_sync_capabilities":{ + "applicable_sync_types":[ + "SYNC_ALL" + ], + "can_commit":false, + "can_delete":false, + "can_void":true, + "is_sync_supported":true, + "supported_countries":[ + "US", + "EU", + "GB", "ALL" + ] + }, + "invoice_sync_capabilities":{ + "applicable_sync_types":[ + "SYNC_ALL" + ], + "can_commit":false, + "can_delete":false, + "can_void":true, + "is_sync_supported":true, + "supported_countries":[ + "US", + "EU", + "GB", "ALL" + ] + }, + "is_consistent_pricing_supported":false, + "supported_countries":[ + "US", + "EU", + "GB", "ALL" + ], + "supported_currencies":[ + "US_DOLLARS" + ], + "tax_calculation_capabilities":{ + "accept_invalid_tax_reg_numbers":false, + "supportedNumberOfLineItems":1200 + } + }, + "customer_identifiers":[ + { + "external_id": "additionalTaxRegistrationNumber", + "display_name": "Additional Tax Registration Number", + "is_mandatory": false, + "field_type": "text" + } + ], + "identity_configuration":{ + "consent_policy_url":" https://www.anrok.com/privacy-terms", + "display_name":"Anrok", + "documentation_url":"https://www.chargebee.com/docs/2.0/anrok.html", + "id":"anrok", + "logo_url":"https://app.anrok.com/images/anrok-chargebee-app-icon.png", + "primary_description":[ + "Anrok unifies sales tax monitoring, calculation and remittance across your financial stack." + ], + "privacy_policy_url":" https://www.anrok.com/privacy-terms", + "secondary_description":[ + "Future-proof your Internet revenue." + ], + "signup_url":"https://anrok.com/contact/request-demo", + "support_email":"support@anrok.com", + "terms_of_service_url":"https://www.anrok.com/privacy-terms" + }, + "product_identifiers":[ + ], + "supported_number_of_line_items":1000, + "version":"1.0.0" + } +} \ No newline at end of file