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

ResponseValidator: Handle "writeOnly" properties #233

Merged
merged 12 commits into from
May 5, 2022
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,10 @@
import java.math.BigInteger;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collector;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;

/**
* Base class for response content decoders.
Expand Down Expand Up @@ -161,19 +156,6 @@ public Optional<JsonNode> decodeNull( String content)
.map( empty -> NullNode.instance);
}

/**
* A collector that produces a map sorted in insertion order.
*/
protected static <T> Collector<T,?,Map<String,String>> toOrderedMap( Function<T,String> keyMapper, Function<T,String> valueMapper)
{
return
toMap(
keyMapper,
valueMapper,
(v1, v2) -> v1,
LinkedHashMap::new);
}

/**
* Decodes an application/x-www-form-urlencoded string.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//////////////////////////////////////////////////////////////////////////////
//
// Copyright 2022, Cornutum Project
// www.cornutum.org
//
//////////////////////////////////////////////////////////////////////////////

package org.cornutum.tcases.openapi.test;

import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;

/**
* Defines methods for handling collections.
*/
public final class CollectionUtils
{
/**
* Creates a new CollectionUtils instance.
*/
private CollectionUtils()
{
// Static methods only.
}


/**
* Returns a stream that produces the sequence defined by the given Iterator.
*/
public static <T> Stream<T> toStream( Iterator<T> iterator)
{
return
Optional.ofNullable( iterator)
.map( i -> {
Iterable<T> iterable = () -> i;
return toStream( iterable);
})
.orElse( null);
}

/**
* Returns a stream that produces the sequence defined by the given Iterable.
*/
public static <T> Stream<T> toStream( Iterable<T> iterable)
{
return
Optional.ofNullable( iterable)
.map( i -> StreamSupport.stream( i.spliterator(), false))
.orElse( null);
}

/**
* A collector that produces a map sorted in insertion order.
*/
public static <T,K,V> Collector<T,?,Map<K,V>> toOrderedMap( Function<T,K> keyMapper, Function<T,V> valueMapper)
{
return
toMap(
keyMapper,
valueMapper,
(v1, v2) -> v1,
LinkedHashMap::new);
}

/**
* Returns a new list contains the head elements followed by the tail elements.
*/
@SafeVarargs
public static <T> List<T> concatList( List<T> head, T... tail)
{
return
Stream.concat(
Optional.ofNullable( head).map( List::stream).orElse( Stream.empty()),
Arrays.stream( tail))
.collect( toList());
}

/**
* Returns a stream that concatenates the members of the given streams.
*/
@SafeVarargs
public static <T> Stream<T> concatStream( Stream<T>... streams)
{
return concatStream( Arrays.asList( streams));
}

/**
* Returns a stream that concatenates the members of the given streams.
*/
public static <T> Stream<T> concatStream( List<Stream<T>> streams)
{
return
streams.isEmpty()
? Stream.empty()
: Stream.concat( streams.get(0), concatStream( streams.subList( 1, streams.size())));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@

package org.cornutum.tcases.openapi.test;

import static org.cornutum.tcases.openapi.test.CollectionUtils.toOrderedMap;
import static org.cornutum.tcases.openapi.test.JsonUtils.createObjectNode;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.util.Arrays;
Expand Down Expand Up @@ -122,7 +124,7 @@ private List<JsonNode> decodeObject( List<Map.Entry<String,String>> properties)
{
return
properties.isEmpty()?
singletonList( mapper_.createObjectNode()) :
singletonList( createObjectNode()) :

// To preserve input order, recursively traverse entries depth-first.
decodeObject( properties.subList( 0, properties.size() - 1)).stream()
Expand All @@ -131,7 +133,7 @@ private List<JsonNode> decodeObject( List<Map.Entry<String,String>> properties)
return
valueDecoder_.decode( properties.get( properties.size() - 1).getValue()).stream()
.map( jsonNode -> {
ObjectNode nextObject = mapper_.createObjectNode();
ObjectNode nextObject = createObjectNode();
nextObject = nextObject.setAll( prevObject);
nextObject = nextObject.set( properties.get( properties.size() - 1).getKey(), jsonNode);
return nextObject;
Expand All @@ -141,5 +143,4 @@ private List<JsonNode> decodeObject( List<Map.Entry<String,String>> properties)
}

private final SimpleDecoder valueDecoder_;
private final ObjectMapper mapper_ = new ObjectMapper();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
//////////////////////////////////////////////////////////////////////////////
//
// Copyright 2022, Cornutum Project
// www.cornutum.org
//
//////////////////////////////////////////////////////////////////////////////

package org.cornutum.tcases.openapi.test;

import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
import static java.util.stream.Collectors.toList;

/**
* Defines methods for processing JSON values.
*/
public final class JsonUtils
{
/**
* Creates a new JsonUtils instance.
*/
private JsonUtils()
{
// Static methods only.
}

/**
* Returns the given JSON node as an object node.
*/
public static ObjectNode expectObject( JsonNode node)
{
return
isMissing( node)
? null
: asObject( node).orElseThrow( () -> new IllegalStateException( String.format( "Expected type=OBJECT, found type=%s", node.getNodeType())));
}

/**
* Returns the given JSON node if it is an object node. Otherwise returns {@link Optional#empty}.
*/
public static Optional<ObjectNode> asObject( JsonNode node)
{
return
isMissing( node)
? Optional.empty()
: Optional.of( node).filter( JsonNode::isObject).map( object -> (ObjectNode) object);
}

/**
* Returns the given JSON node if it is an array node. Otherwise returns {@link Optional#empty}.
*/
public static Optional<ArrayNode> asArray( JsonNode node)
{
return
isMissing( node)
? Optional.empty()
: Optional.of( node).filter( JsonNode::isArray).map( array -> (ArrayNode) array);
}

/**
* Returns true if the given JSON node is null or missing.
*/
public static boolean isMissing( JsonNode node)
{
return
Optional.ofNullable( node)
.map( JsonNode::isMissingNode)
.orElse( true);
}

/**
* Returns true if the given string represents an integer
*/
public static boolean isInteger( String value)
{
try
{
Integer.parseInt( value);
return true;
}
catch( Exception e)
{
return false;
}
}

/**
* Returns a JSON pointer for the given path
*/
public static JsonPointer pointer( String... path)
{
return pointer( Arrays.asList( path));
}

/**
* Returns a JSON pointer for the given path
*/
public static JsonPointer pointer( Iterable<String> path)
{
StringJoiner joiner = new StringJoiner( "/", "/", "");
for( String name : path)
{
joiner.add( pointerSegment( name));
}

return JsonPointer.compile( joiner.toString());
}

/**
* Returns the given name as JSON Pointer segment, escaping special characters as defined in <A href="https://datatracker.ietf.org/doc/html/rfc6901">RFC6901</A>.
*/
private static String pointerSegment( String name)
{
Matcher matcher = POINTER_ESCAPES.matcher( name);
StringBuffer escaped = new StringBuffer();
while( matcher.find())
{
matcher.appendReplacement(
escaped,
matcher.group(1) != null? "~0" : "~1");
}
matcher.appendTail( escaped);
return escaped.toString();
}

/**
* Returns the final segment of the given JSON pointer.
*/
public static String tailOf( JsonPointer pointer)
{
return
Optional.of( pathOf( pointer))
.filter( path -> !path.isEmpty())
.map( path -> path.get( path.size() - 1))
.orElse( "");
}

/**
* Returns the path represented by the given JSON pointer.
*/
public static List<String> pathOf( JsonPointer pointer)
{
String[] segments = String.valueOf( pointer).split( "/", 0);
return
IntStream.range( 1, segments.length)
.mapToObj( i -> pathElementOf( segments[i]))
.collect( toList());
}

/**
* Returns the path element represented by the given JSON pointer segment.
*/
private static String pathElementOf( String segment)
{
Matcher matcher = POINTER_ESCAPED.matcher( segment);
StringBuffer unescaped = new StringBuffer();
while( matcher.find())
{
matcher.appendReplacement(
unescaped,
matcher.group(1) != null? "~" : "/");
}
matcher.appendTail( unescaped);
return unescaped.toString();
}

/**
* Returns a new empty ObjectNode.
*/
public static ObjectNode createObjectNode()
{
return mapper().createObjectNode();
}

/**
* Returns a new empty ArrayNode.
*/
public static ArrayNode createArrayNode()
{
return mapper().createArrayNode();
}

/**
* Returns a JSON ObjectMapper.
*/
public static ObjectMapper mapper()
{
return mapper_;
}

private static final Pattern POINTER_ESCAPES = Pattern.compile( "(~)|(/)");
private static final Pattern POINTER_ESCAPED = Pattern.compile( "(~0)|(~1)");
private static final ObjectMapper mapper_ = new ObjectMapper();
}
Loading