diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/OpenAPIDeserializer.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/OpenAPIDeserializer.java index c03ab661e8..fd0c604b3b 100644 --- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/OpenAPIDeserializer.java +++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/OpenAPIDeserializer.java @@ -17,8 +17,11 @@ import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.ByteArraySchema; import io.swagger.v3.oas.models.media.ComposedSchema; import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.DateSchema; +import io.swagger.v3.oas.models.media.DateTimeSchema; import io.swagger.v3.oas.models.media.Discriminator; import io.swagger.v3.oas.models.media.Encoding; import io.swagger.v3.oas.models.media.MediaType; @@ -56,10 +59,13 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.text.ParseException; import java.util.*; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; +import static java.util.Calendar.*; public class OpenAPIDeserializer { @@ -94,6 +100,9 @@ public class OpenAPIDeserializer { private static final String COOKIE_PARAMETER = "cookie"; private static final String PATH_PARAMETER = "path"; private static final String HEADER_PARAMETER = "header"; + private static final Pattern RFC3339_DATE_TIME_PATTERN = Pattern.compile( "^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(\\.\\d+)?((Z)|([+-]\\d{2}:\\d{2}))$"); + private static final Pattern RFC3339_DATE_PATTERN = Pattern.compile( "^(\\d{4})-(\\d{2})-(\\d{2})$"); + private Components components; private final Set operationIDs = new HashSet<>(); @@ -2201,8 +2210,13 @@ public Schema getSchema(ObjectNode node, String location, ParseResult result){ for (JsonNode n : enumArray) { if (n.isNumber()) { schema.addEnumItemObject(n.numberValue()); - }else if (n.isValueNode()) { - schema.addEnumItemObject(n.asText()); + } else if (n.isValueNode()) { + try { + schema.addEnumItemObject( getDecodedObject( schema, n.asText(null))); + } + catch( ParseException e) { + result.invalidType( location, String.format( "enum=`%s`", e.getMessage()), schema.getFormat(), n); + } } else { result.invalidType(location, "enum", "value", n); } @@ -2291,7 +2305,12 @@ public Schema getSchema(ObjectNode node, String location, ParseResult result){ }else if(schema.getType().equals("string")) { value = getString("default", node, false, location, result); if (value != null) { - schema.setDefault(value); + try { + schema.setDefault( getDecodedObject( schema, value)); + } + catch( ParseException e) { + result.invalidType( location, String.format( "default=`%s`", e.getMessage()), schema.getFormat(), node); + } } }else if(schema.getType().equals("boolean")) { bool = getBoolean("default", node, false, location, result); @@ -2375,6 +2394,121 @@ public Schema getSchema(ObjectNode node, String location, ParseResult result){ } + /** + * Decodes the given string and returns an object applicable to the given schema. + * Throws a ParseException if no applicable object can be recognized. + */ + private Object getDecodedObject( Schema schema, String objectString) throws ParseException { + Object object = + objectString == null? + null : + + schema.getClass().equals( DateSchema.class)? + toDate( objectString) : + + schema.getClass().equals( DateTimeSchema.class)? + toDateTime( objectString) : + + schema.getClass().equals( ByteArraySchema.class)? + toBytes( objectString) : + + objectString; + + if( object == null && objectString != null) { + throw new ParseException( objectString, 0); + } + + return object; + } + + + /** + * Returns the Date represented by the given RFC3339 date-time string. + * Returns null if this string can't be parsed as Date. + */ + private Date toDateTime( String dateString) { + // Note: For this conversion, regex matching is better than SimpleDateFormat, etc. + // Optional elements (e.g. milliseconds) are not directly handled by SimpleDateFormat. + // Also, SimpleDateFormat is not thread-safe. + Matcher matcher = RFC3339_DATE_TIME_PATTERN.matcher( dateString); + + Date dateTime = null; + if( matcher.matches()) { + try { + String year = matcher.group(1); + String month = matcher.group(2); + String day = matcher.group(3); + String hour = matcher.group(4); + String min = matcher.group(5); + String sec = matcher.group(6); + String ms = matcher.group(7); + String zone = matcher.group(10); + + Calendar calendar = Calendar.getInstance( TimeZone.getTimeZone( zone == null? "GMT" : "GMT" + zone)); + calendar.set( YEAR, Integer.parseInt( year)); + calendar.set( MONTH, Integer.parseInt( month) - 1); + calendar.set( DAY_OF_MONTH, Integer.parseInt( day)); + calendar.set( HOUR_OF_DAY, Integer.parseInt( hour)); + calendar.set( MINUTE, Integer.parseInt( min)); + calendar.set( SECOND, Integer.parseInt( sec)); + calendar.set( MILLISECOND, ms == null? 0 : (int) (Double.parseDouble( ms) * 1000)); + + dateTime = calendar.getTime(); + } + catch( Exception ignore) { + } + } + + return dateTime; + } + + + /** + * Returns the Date represented by the given RFC3339 full-date string. + * Returns null if this string can't be parsed as Date. + */ + private Date toDate( String dateString) { + Matcher matcher = RFC3339_DATE_PATTERN.matcher( dateString); + + Date date = null; + if( matcher.matches()) { + String year = matcher.group(1); + String month = matcher.group(2); + String day = matcher.group(3); + + try { + date= + new Calendar.Builder() + .setDate( Integer.parseInt( year), Integer.parseInt( month) - 1, Integer.parseInt( day)) + .build() + .getTime(); + } + catch( Exception ignore) { + } + } + + return date; + } + + + /** + * Returns the byte array represented by the given base64-encoded string. + * Returns null if this string is not a valid base64 encoding. + */ + private byte[] toBytes( String byteString) { + byte[] bytes; + + try { + bytes = Base64.getDecoder().decode( byteString); + } + catch( Exception e) { + bytes = null; + } + + return bytes; + } + + public Map getExamples(ObjectNode obj, String location, ParseResult result) { diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/OpenAPIDeserializerTest.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/OpenAPIDeserializerTest.java index 49b6f13938..dca3900b56 100644 --- a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/OpenAPIDeserializerTest.java +++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/OpenAPIDeserializerTest.java @@ -13,8 +13,11 @@ import io.swagger.v3.oas.models.headers.Header; import io.swagger.v3.oas.models.media.ArraySchema; import io.swagger.v3.oas.models.media.BinarySchema; +import io.swagger.v3.oas.models.media.ByteArraySchema; import io.swagger.v3.oas.models.media.ComposedSchema; import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.DateSchema; +import io.swagger.v3.oas.models.media.DateTimeSchema; import io.swagger.v3.oas.models.media.IntegerSchema; import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.media.ObjectSchema; @@ -45,10 +48,14 @@ import java.math.BigDecimal; import java.nio.file.Files; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TimeZone; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -796,6 +803,222 @@ public void testDeserializeEnum() { assertEquals(numberImpl.getDefault(), new BigDecimal("3.14")); } + @Test + public void testDeserializeDateString() { + String yaml = "openapi: 3.0.0\n" + + "servers: []\n" + + "info:\n" + + " version: 0.0.0\n" + + " title: My Title\n" + + "paths:\n" + + " /persons:\n" + + " get:\n" + + " description: a test\n" + + " responses:\n" + + " '200':\n" + + " description: Successful response\n" + + " content:\n" + + " '*/*':\n" + + " schema:\n" + + " type: object\n" + + " properties:\n" + + " date:\n" + + " $ref: '#/components/schemas/DateString'\n" + + "components:\n" + + " schemas:\n" + + " DateString:\n" + + " type: string\n" + + " format: date\n" + + " default: 2019-01-01\n" + + " enum:\n" + + " - 2019-01-01\n" + + " - Nope\n" + + " - 2018-02-02\n" + + " - 2017-03-03\n" + + " - null\n" + + ""; + OpenAPIV3Parser parser = new OpenAPIV3Parser(); + SwaggerParseResult result = parser.readContents(yaml, null, null); + + final OpenAPI resolved = new OpenAPIResolver(result.getOpenAPI(), null).resolve(); + + Schema dateModel = resolved.getComponents().getSchemas().get("DateString"); + assertTrue(dateModel instanceof DateSchema); + List dateValues = dateModel.getEnum(); + assertEquals(dateValues.size(), 4); + assertEquals( + dateValues.get(0), + new Calendar.Builder().setDate( 2019, 0, 1).build().getTime()); + assertEquals( + dateValues.get(1), + new Calendar.Builder().setDate( 2018, 1, 2).build().getTime()); + assertEquals( + dateValues.get(2), + new Calendar.Builder().setDate( 2017, 2, 3).build().getTime()); + assertEquals( + dateValues.get(3), + null); + + assertEquals( + dateModel.getDefault(), + new Calendar.Builder().setDate( 2019, 0, 1).build().getTime()); + + assertEquals( + result.getMessages(), + Arrays.asList( "attribute components.schemas.DateString.enum=`Nope` is not of type `date`")); + } + + @Test + public void testDeserializeDateTimeString() { + String yaml = "openapi: 3.0.0\n" + + "servers: []\n" + + "info:\n" + + " version: 0.0.0\n" + + " title: My Title\n" + + "paths:\n" + + " /persons:\n" + + " get:\n" + + " description: a test\n" + + " responses:\n" + + " '200':\n" + + " description: Successful response\n" + + " content:\n" + + " '*/*':\n" + + " schema:\n" + + " type: object\n" + + " properties:\n" + + " dateTime:\n" + + " $ref: '#/components/schemas/DateTimeString'\n" + + "components:\n" + + " schemas:\n" + + " DateTimeString:\n" + + " type: string\n" + + " format: date-time\n" + + " default: 2019-01-01T00:00:00Z\n" + + " enum:\n" + + " - null\n" + + " - Nunh uh\n" + + " - 2019-01-01T00:00:00Z\n" + + " - 2018-02-02T23:59:59.999-05:00\n" + + " - 2017-03-03T11:22:33+06:00\n" + + " - 2016-04-04T22:33:44.555Z\n" + + ""; + OpenAPIV3Parser parser = new OpenAPIV3Parser(); + SwaggerParseResult result = parser.readContents(yaml, null, null); + + final OpenAPI resolved = new OpenAPIResolver(result.getOpenAPI(), null).resolve(); + + Schema dateTimeModel = resolved.getComponents().getSchemas().get("DateTimeString"); + assertTrue(dateTimeModel instanceof DateTimeSchema); + List dateTimeValues = dateTimeModel.getEnum(); + assertEquals(dateTimeValues.size(), 5); + assertEquals( + dateTimeValues.get(0), + null); + assertEquals( + dateTimeValues.get(1), + new Calendar.Builder() + .setDate( 2019, 0, 1) + .setTimeOfDay( 0, 0, 0, 0) + .setTimeZone( TimeZone.getTimeZone( "GMT")) + .build() + .getTime()); + assertEquals( + dateTimeValues.get(2), + new Calendar.Builder() + .setDate( 2018, 1, 2) + .setTimeOfDay( 23, 59, 59, 999) + .setTimeZone( TimeZone.getTimeZone( "GMT-05:00")) + .build() + .getTime()); + assertEquals( + dateTimeValues.get(3), + new Calendar.Builder() + .setDate( 2017, 2, 3) + .setTimeOfDay( 11, 22, 33, 0) + .setTimeZone( TimeZone.getTimeZone( "GMT+06:00")) + .build() + .getTime()); + assertEquals( + dateTimeValues.get(4), + new Calendar.Builder() + .setDate( 2016, 3, 4) + .setTimeOfDay( 22, 33, 44, 555) + .setTimeZone( TimeZone.getTimeZone( "GMT")) + .build() + .getTime()); + + assertEquals( + dateTimeModel.getDefault(), + new Calendar.Builder() + .setDate( 2019, 0, 1) + .setTimeOfDay( 0, 0, 0, 0) + .setTimeZone( TimeZone.getTimeZone( "GMT")) + .build() + .getTime()); + + assertEquals( + result.getMessages(), + Arrays.asList( "attribute components.schemas.DateTimeString.enum=`Nunh uh` is not of type `date-time`")); + } + + @Test + public void testDeserializeByteString() { + String yaml = "openapi: 3.0.0\n" + + "servers: []\n" + + "info:\n" + + " version: 0.0.0\n" + + " title: My Title\n" + + "paths:\n" + + " /persons:\n" + + " get:\n" + + " description: a test\n" + + " responses:\n" + + " '200':\n" + + " description: Successful response\n" + + " content:\n" + + " '*/*':\n" + + " schema:\n" + + " type: object\n" + + " properties:\n" + + " bytes:\n" + + " $ref: '#/components/schemas/ByteString'\n" + + "components:\n" + + " schemas:\n" + + " ByteString:\n" + + " type: string\n" + + " format: byte\n" + + " default: W.T.F?\n" + + " enum:\n" + + " - VGhlIHdvcmxk\n" + + " - aXMgYWxs\n" + + " - dGhhdCBpcw==\n" + + " - dGhlIGNhc2U=\n" + + " - W.T.F?\n" + + ""; + OpenAPIV3Parser parser = new OpenAPIV3Parser(); + SwaggerParseResult result = parser.readContents(yaml, null, null); + + final OpenAPI resolved = new OpenAPIResolver(result.getOpenAPI(), null).resolve(); + + Schema byteModel = resolved.getComponents().getSchemas().get("ByteString"); + assertTrue(byteModel instanceof ByteArraySchema); + List byteValues = byteModel.getEnum(); + assertEquals(byteValues.size(), 4); + assertEquals(new String( byteValues.get(0)), "The world"); + assertEquals(new String( byteValues.get(1)), "is all"); + assertEquals(new String( byteValues.get(2)), "that is"); + assertEquals(new String( byteValues.get(3)), "the case"); + + assertEquals( byteModel.getDefault(), null); + + assertEquals( + result.getMessages(), + Arrays.asList( + "attribute components.schemas.ByteString.enum=`W.T.F?` is not of type `byte`", + "attribute components.schemas.ByteString.default=`W.T.F?` is not of type `byte`")); + } + @Test public void testDeserializeWithMessages() { String yaml = "openapi: '3.0.0'\n" +