1818
1919import io .swagger .v3 .oas .models .OpenAPI ;
2020import io .swagger .v3 .oas .models .media .Schema ;
21+ import io .swagger .v3 .oas .models .parameters .RequestBody ;
2122import org .apache .commons .io .FileUtils ;
2223import org .openapitools .codegen .*;
2324import org .openapitools .codegen .model .ModelMap ;
@@ -394,6 +395,12 @@ public void processOpenAPI(OpenAPI openAPI) {
394395 }
395396
396397
398+ /**
399+ * This class is used in pathExtractorParams.mustache.
400+ *
401+ * It exposes some methods which make it more readable
402+ * for that mustache snippet, and also isolates the logic needed for the path extractors
403+ */
397404 public static class ParamPart {
398405 final CodegenParameter param ;
399406 final String name ;
@@ -416,7 +423,9 @@ public ParamPart(String name, CodegenParameter param) {
416423 }
417424
418425 /**
419- * Cask will compile but 'initialize' can throw a route overlap exception:
426+ * This data structure is here to manually identify and fix routes which will overlap (e.g. GET /foo/bar and GET /foo/bazz)
427+ *
428+ * If we added these as individual routes, then Cask itself will compile, but calling 'initialize' throws a route overlap exception:
420429 * <p>
421430 * {{{
422431 * Routes overlap with wildcards: get /user/logout, get /user/:username, get /user/login
@@ -672,9 +681,12 @@ private void postProcessModel(CodegenModel model) {
672681
673682 model .getVars ().forEach (this ::postProcessProperty );
674683 model .getAllVars ().forEach (this ::postProcessProperty );
684+
685+
686+ model .vendorExtensions .put ("x-has-one-of" , model .oneOf != null && !model .oneOf .isEmpty ());
675687 }
676688
677- private static void postProcessOperation (CodegenOperation op ) {
689+ private static void postProcessOperation (final CodegenOperation op ) {
678690 // force http method to lower case
679691 op .httpMethod = op .httpMethod .toLowerCase (Locale .ROOT );
680692
@@ -710,9 +722,33 @@ private static void postProcessOperation(CodegenOperation op) {
710722 .collect (Collectors .toCollection (LinkedHashSet ::new ));
711723
712724 var responseType = responses .isEmpty () ? "Unit" : String .join (" | " , responses );
725+ op .vendorExtensions .put ("x-import-response-implicits" , importResponseImplicits (op ));
713726 op .vendorExtensions .put ("x-response-type" , responseType );
714727 }
715728
729+ /**
730+ * We need to bring the response type into scope in order to use the upickle implicits
731+ * only if the response type has a 'oneOf' type, which means it's a union type with a
732+ * companion object containing the ReadWriter
733+ *
734+ * @param op
735+ * @return true if we need to provide an import
736+ */
737+ private static boolean importResponseImplicits (final CodegenOperation op ) {
738+ final Set <String > importBlacklist = Set .of ("File" );
739+
740+ boolean doImport = false ;
741+ for (var response : op .responses ) {
742+ // we should ignore generic types like Seq[...] or Map[..] types
743+ var isPolymorphic = response .dataType != null && response .dataType .contains ("[" );
744+ if (response .isModel && !importBlacklist .contains (response .dataType ) && !isPolymorphic ) {
745+ doImport = true ;
746+ break ;
747+ }
748+ }
749+ return doImport ;
750+ }
751+
716752 /**
717753 * primitive or enum types don't have Data representations
718754 * @param p the property
@@ -747,6 +783,10 @@ private static boolean isByteArray(final CodegenProperty p) {
747783 return "byte" .equalsIgnoreCase (p .dataFormat ); // &&
748784 }
749785
786+ private static boolean wrapInOptional (CodegenProperty p ) {
787+ return !p .required && !p .isArray && !p .isMap ;
788+ }
789+
750790 /**
751791 * this parameter is used to create the function:
752792 * {{{
@@ -761,19 +801,18 @@ private static boolean isByteArray(final CodegenProperty p) {
761801 * and then back again
762802 */
763803 private static String asDataCode (final CodegenProperty p , final Set <String > typesWhichDoNotNeedMapping ) {
764- final var wrapInOptional = !p .required && !p .isArray && !p .isMap ;
765804 String code = "" ;
766805
767806 String dv = defaultValueNonOption (p , p .defaultValue );
768807
769808 if (doesNotNeedMapping (p , typesWhichDoNotNeedMapping )) {
770- if (wrapInOptional ) {
809+ if (wrapInOptional ( p ) ) {
771810 code = String .format (Locale .ROOT , "%s.getOrElse(%s) /* 1 */" , p .name , dv );
772811 } else {
773812 code = String .format (Locale .ROOT , "%s /* 2 */" , p .name );
774813 }
775814 } else {
776- if (wrapInOptional ) {
815+ if (wrapInOptional ( p ) ) {
777816 if (isByteArray (p )) {
778817 code = String .format (Locale .ROOT , "%s.getOrElse(%s) /* 3 */" , p .name , dv );
779818 } else {
@@ -782,11 +821,15 @@ private static String asDataCode(final CodegenProperty p, final Set<String> type
782821 } else if (p .isArray ) {
783822 if (isByteArray (p )) {
784823 code = String .format (Locale .ROOT , "%s /* 5 */" , p .name );
824+ } else if (!isObjectArray (p )) {
825+ code = String .format (Locale .ROOT , "%s /* 5.1 */" , p .name );
785826 } else {
786827 code = String .format (Locale .ROOT , "%s.map(_.asData) /* 6 */" , p .name );
787828 }
829+ } else if (p .isMap ) {
830+ code = String .format (Locale .ROOT , "%s /* 7 */" , p .name );
788831 } else {
789- code = String .format (Locale .ROOT , "%s.asData /* 7 */" , p .name );
832+ code = String .format (Locale .ROOT , "%s.asData /* 8 */" , p .name );
790833 }
791834 }
792835 return code ;
@@ -807,24 +850,25 @@ private static String asDataCode(final CodegenProperty p, final Set<String> type
807850 * @return
808851 */
809852 private static String asModelCode (final CodegenProperty p , final Set <String > typesWhichDoNotNeedMapping ) {
810- final var wrapInOptional = !p .required && !p .isArray && !p .isMap ;
811853 String code = "" ;
812854
813855 if (doesNotNeedMapping (p , typesWhichDoNotNeedMapping )) {
814- if (wrapInOptional ) {
856+ if (wrapInOptional ( p ) ) {
815857 code = String .format (Locale .ROOT , "Option(%s) /* 1 */" , p .name );
816858 } else {
817859 code = String .format (Locale .ROOT , "%s /* 2 */" , p .name );
818860 }
819861 } else {
820- if (wrapInOptional ) {
862+ if (wrapInOptional ( p ) ) {
821863 if (isByteArray (p )) {
822864 code = String .format (Locale .ROOT , "Option(%s) /* 3 */" , p .name );
823865 } else {
824866 code = String .format (Locale .ROOT , "Option(%s).map(_.asModel) /* 4 */" , p .name );
825867 }
826868 } else if (p .isArray ) {
827869 code = String .format (Locale .ROOT , "%s.map(_.asModel) /* 5 */" , p .name );
870+ } else if (p .isMap ) {
871+ code = String .format (Locale .ROOT , "%s /* 5.1 */" , p .name );
828872 } else {
829873 code = String .format (Locale .ROOT , "%s.asModel /* 6 */" , p .name );
830874 }
@@ -863,8 +907,17 @@ private String ensureNonKeyword(String text) {
863907 return text ;
864908 }
865909
910+ private static boolean hasItemModel (final CodegenProperty p ) {
911+ return p .items != null && p .items .isModel ;
912+ }
913+
914+ private static boolean isObjectArray (final CodegenProperty p ) {
915+ return p .isArray && hasItemModel (p );
916+ }
917+
866918 private void postProcessProperty (final CodegenProperty p ) {
867- p .vendorExtensions .put ("x-datatype-model" , asScalaDataType (p , p .required , false ));
919+
920+ p .vendorExtensions .put ("x-datatype-model" , asScalaDataType (p , p .required , false , wrapInOptional (p )));
868921 p .vendorExtensions .put ("x-defaultValue-model" , defaultValue (p , p .required , p .defaultValue ));
869922 final String dataTypeData = asScalaDataType (p , p .required , true );
870923 p .vendorExtensions .put ("x-datatype-data" , dataTypeData );
@@ -878,7 +931,7 @@ private void postProcessProperty(final CodegenProperty p) {
878931 p ._enum = p ._enum .stream ().map (this ::ensureNonKeyword ).collect (Collectors .toList ());
879932 }
880933
881- /**
934+ /*
882935 * This is a fix for the enum property "type" declared like this:
883936 * {{{
884937 * type:
@@ -908,6 +961,9 @@ private void postProcessProperty(final CodegenProperty p) {
908961 )).collect (Collectors .toSet ());
909962 typesWhichShouldNotBeMapped .add ("byte" );
910963
964+ // when deserialising map objects, the logic is tricky.
965+ p .vendorExtensions .put ("x-deserialize-asModelMap" , p .isMap && hasItemModel (p ));
966+
911967 // the 'asModel' logic for modelData.mustache
912968 //
913969 // if it's optional (not required), then wrap the value in Option()
@@ -916,16 +972,6 @@ private void postProcessProperty(final CodegenProperty p) {
916972 p .vendorExtensions .put ("x-asData" , asDataCode (p , typesWhichShouldNotBeMapped ));
917973 p .vendorExtensions .put ("x-asModel" , asModelCode (p , typesWhichShouldNotBeMapped ));
918974
919- // if it's an array or optional, we need to map it as a model -- unless it's a map,
920- // in which case we have to map the values
921- boolean hasItemModel = p .items != null && p .items .isModel ;
922- boolean isObjectArray = p .isArray && hasItemModel ;
923- boolean isOptionalObj = !p .required && p .isModel ;
924- p .vendorExtensions .put ("x-map-asModel" , (isOptionalObj || isObjectArray ) && !p .isMap );
925-
926- // when deserialising map objects, the logic is tricky.
927- p .vendorExtensions .put ("x-deserialize-asModelMap" , p .isMap && hasItemModel );
928-
929975 // for some reason, an openapi spec with pattern field like this:
930976 // pattern: '^[A-Za-z]+$'
931977 // will result in the pattern property text of
@@ -934,6 +980,20 @@ private void postProcessProperty(final CodegenProperty p) {
934980 p .pattern = p .pattern .substring (1 , p .pattern .length () - 1 );
935981 }
936982
983+ // in our model class definition laid out in modelClass.mustache, we use 'Option' for non-required
984+ // properties only when they don't have a sensible 'empty' value (e.g. maps and lists).
985+ //
986+ // that is to say, we're trying to avoid having:
987+ //
988+ // someOptionalField : Option[Seq[Foo]]
989+ //
990+ // when we could just have e.g.
991+ //
992+ // someOptionalField : Seq[Foo]
993+ //
994+ // with an empty value
995+ p .vendorExtensions .put ("x-model-needs-option" , wrapInOptional (p ));
996+
937997 }
938998
939999
0 commit comments