skyscreamer / JSONassert

Write JSON unit tests in less code. Great for testing REST interfaces.
http://jsonassert.skyscreamer.org
Apache License 2.0
994 stars 197 forks source link

make method findUniqueKey in AbstractComparator customizable #180

Open astmuc opened 9 months ago

astmuc commented 9 months ago

The method JSONCompareUtil#findUniqueKey is used as static. I am using an JsonComparator derived from DefaultComparator which ignores string-like properties if they match some pattern. But such properties may be elected as unique keys for faster array comparison. The comparison fails in this case even if the property is a one to ignore.

The feature request is to make this logic customizable.

My example with a work-aound is:

package ppm.testsupport;

import io.vavr.collection.HashMap;
import io.vavr.collection.Map;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.skyscreamer.jsonassert.JSONCompareResult;
import org.skyscreamer.jsonassert.comparator.DefaultComparator;
import org.skyscreamer.jsonassert.comparator.JSONComparator;

import java.util.regex.Pattern;

import static org.skyscreamer.jsonassert.comparator.JSONCompareUtil.*;

/**
 * This class is a lenient JSON comparator that extends the DefaultComparator class.
 * It provides additional functionality to ignore certain patterns in JSON values
 * during comparison.
 */
public class SuperLenientJsonComparator extends DefaultComparator {

  public static JSONComparator LENIENT = new SuperLenientJsonComparator( JSONCompareMode.LENIENT );

  private final Map<Pattern, String> patternsToIgnore = HashMap.of(
      Pattern.compile( "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{1,9}" ), "LocalDateTime",
      Pattern.compile( "ppm:core.cid:\\p{XDigit}{8}-\\p{XDigit}{4}-\\p{XDigit}{4}-\\p{XDigit}{4}-\\p{XDigit}{12}" ), "CorrelationId",
      Pattern.compile( "ppm:core.eev:\\p{XDigit}{8}-\\p{XDigit}{4}-\\p{XDigit}{4}-\\p{XDigit}{4}-\\p{XDigit}{12}:\\d+" ), "EventId",
      Pattern.compile( "\\p{XDigit}{8}-\\p{XDigit}{4}-\\p{XDigit}{4}-\\p{XDigit}{4}-\\p{XDigit}{12}" ), "UUID",
      Pattern.compile( "ppm.core.s:.*" ), "SchedulerId",
      Pattern.compile( "PT[.0-9]*S$" ), "Duration"
  );

  public SuperLenientJsonComparator( JSONCompareMode mode ) {
    super( mode );
  }

  @Override
  public void compareValues( String prefix, Object expectedValue, Object actualValue, JSONCompareResult result ) throws JSONException {
    boolean ignore = false;
    if ( expectedValue instanceof String expected && actualValue instanceof String actual ) {
      for ( Pattern pattern : patternsToIgnore.keySet() ) {
        if ( pattern.matcher( actual ).matches() && pattern.matcher( expected ).matches() ) {
          ignore = true;
          break;
        }
      }
    }
    if ( !ignore ) {
      super.compareValues( prefix, expectedValue, actualValue, result );
    }
  }

  private String findUniqueKeySkipIgnored( JSONArray expected ) throws JSONException {
    JSONObject o = (JSONObject) expected.get( 0 ); // There's at least one at this point
    searchLoop:
    for ( String candidate : getKeys( o ) ) {
      var candidateValue = o.get( candidate );
      if ( candidateValue instanceof String candidateValueString ) {
        for ( var pattern : patternsToIgnore.keySet() ) {
          if ( (pattern.matcher( candidateValueString )).matches() ) {
            continue searchLoop;
          }
        }
      }
      if ( isUsableAsUniqueKey( candidate, expected ) )
        return candidate;
    }
    // No usable unique key :-(
    return null;

  }

  /**
   * * code copied from base class, only findUniqueKey is replaced as being static.
   */
  @Override
  protected void compareJSONArrayOfJsonObjects( String key, JSONArray expected, JSONArray actual, JSONCompareResult result ) throws JSONException {
    String uniqueKey = findUniqueKeySkipIgnored( expected );
    if ( uniqueKey == null || !isUsableAsUniqueKey( uniqueKey, actual ) ) {
      // An expensive last resort
      recursivelyCompareJSONArray( key, expected, actual, result );
      return;
    }
    java.util.Map<Object, JSONObject> expectedValueMap = arrayOfJsonObjectToMap( expected, uniqueKey );
    java.util.Map<Object, JSONObject> actualValueMap = arrayOfJsonObjectToMap( actual, uniqueKey );
    for ( Object id : expectedValueMap.keySet() ) {
      if ( !actualValueMap.containsKey( id ) ) {
        result.missing( formatUniqueKey( key, uniqueKey, id ), expectedValueMap.get( id ) );
        continue;
      }
      JSONObject expectedValue = expectedValueMap.get( id );
      JSONObject actualValue = actualValueMap.get( id );
      compareValues( formatUniqueKey( key, uniqueKey, id ), expectedValue, actualValue, result );
    }
    for ( Object id : actualValueMap.keySet() ) {
      if ( !expectedValueMap.containsKey( id ) ) {
        result.unexpected( formatUniqueKey( key, uniqueKey, id ), actualValueMap.get( id ) );
      }
    }
  }

  @Override
  public void compareJSONArray( String prefix, JSONArray expected, JSONArray actual, JSONCompareResult result ) throws JSONException {
    replaceIgnorableStringValuesWithFixedPattern( expected );
    replaceIgnorableStringValuesWithFixedPattern( actual );
    super.compareJSONArray( prefix, expected, actual, result );
  }

  private void replaceIgnorableStringValuesWithFixedPattern( JSONArray array ) throws JSONException {
    for ( int i = 0; i < array.length(); i++ ) {
      if ( !array.isNull( i ) ) {
        var element = array.get( i );
        if ( element instanceof String stringValue ) {
          for ( var pattern : patternsToIgnore ) {
            if ( pattern._1().matcher( stringValue ).matches() ) {
              array.put( i, pattern._2() );
              break;
            }
          }
        }
      }
    }
  }
}
quxiaojing commented 2 months ago

I'am also find a way to manually set uniqueKey , like id . it always select the un-wanted uniqueKey