Skip to content

Commit 31be9b9

Browse files
authored
scala-cask fix: Added support for 'additionalProperties:true' (#19767)
* Added support for 'additionalProperties:true' to scala-cask generator additionalProperties means the request can contain arbitrary additional properties, and so this change adds an 'additionalProperties' field to request objects which is a json type. * fixed warning in example scala-cli project * updated samples * addressed codegen comments
1 parent d60200d commit 31be9b9

30 files changed

+503
-669
lines changed

modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -3158,7 +3158,7 @@ protected void setAddProps(Schema schema, IJsonSchemaValidationProperties proper
31583158
additionalPropertiesIsAnyType = true;
31593159
}
31603160
} else {
3161-
// if additioanl properties is set (e.g. free form object, any type, string, etc)
3161+
// if additional properties is set (e.g. free form object, any type, string, etc)
31623162
addPropProp = fromProperty(getAdditionalPropertiesName(), (Schema) schema.getAdditionalProperties(), false);
31633163
additionalPropertiesIsAnyType = true;
31643164
}

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaCaskServerCodegen.java

+32-33
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements CodegenConfig {
4040
public static final String PROJECT_NAME = "projectName";
4141

42+
// this is our opinionated json type - ujson.Value - which is a first-class
43+
// citizen of cask
44+
private static final String AdditionalPropertiesType = "Value";
45+
4246
private final Logger LOGGER = LoggerFactory.getLogger(ScalaCaskServerCodegen.class);
4347

4448
@Override
@@ -115,6 +119,8 @@ public ScalaCaskServerCodegen() {
115119

116120
typeMapping.put("integer", "Int");
117121
typeMapping.put("long", "Long");
122+
typeMapping.put("AnyType", AdditionalPropertiesType);
123+
118124
//TODO binary should be mapped to byte array
119125
// mapped to String as a workaround
120126
typeMapping.put("binary", "String");
@@ -241,6 +247,7 @@ public void processOpts() {
241247
importMapping.put("OffsetDateTime", "java.time.OffsetDateTime");
242248
importMapping.put("LocalTime", "java.time.LocalTime");
243249
importMapping.put("Value", "ujson.Value");
250+
importMapping.put(AdditionalPropertiesType, "ujson.Value");
244251
}
245252

246253
static boolean consumesMimetype(CodegenOperation op, String mimetype) {
@@ -614,7 +621,7 @@ private void setDefaultValueForCodegenProperty(CodegenProperty p) {
614621
if (p.getIsEnumOrRef()) {
615622
p.defaultValue = "null";
616623
} else {
617-
p.defaultValue = defaultValueNonOption(p);
624+
p.defaultValue = defaultValueNonOption(p, "null");
618625
}
619626
} else if (p.defaultValue.contains("Seq.empty")) {
620627
p.defaultValue = "Nil";
@@ -767,6 +774,23 @@ private static String defaultValue(IJsonSchemaValidationProperties p, boolean re
767774
return defaultValueNonOption(p, fallbackDefaultValue);
768775
}
769776

777+
/**
778+
* the subtypes of IJsonSchemaValidationProperties have an 'isNumeric', but that's not a method on IJsonSchemaValidationProperties.
779+
*
780+
* This helper method tries to isolate that noisy logic in a safe way so we can ask 'is this IJsonSchemaValidationProperties numeric'?
781+
* @param p the property
782+
* @return true if the property is numeric
783+
*/
784+
private static boolean isNumeric(IJsonSchemaValidationProperties p) {
785+
if (p instanceof CodegenParameter) {
786+
return ((CodegenParameter)p).isNumeric;
787+
} else if (p instanceof CodegenProperty) {
788+
return ((CodegenProperty)p).isNumeric;
789+
} else {
790+
return p.getIsNumber() || p.getIsFloat() || p.getIsDecimal() || p.getIsDouble() || p.getIsInteger() || p.getIsLong() || p.getIsUnboundedInteger();
791+
}
792+
}
793+
770794
private static String defaultValueNonOption(IJsonSchemaValidationProperties p, String fallbackDefaultValue) {
771795
if (p.getIsArray()) {
772796
if (p.getUniqueItems()) {
@@ -777,7 +801,7 @@ private static String defaultValueNonOption(IJsonSchemaValidationProperties p, S
777801
if (p.getIsMap()) {
778802
return "Map.empty";
779803
}
780-
if (p.getIsNumber()) {
804+
if (isNumeric(p)) {
781805
return "0";
782806
}
783807
if (p.getIsEnum()) {
@@ -792,37 +816,12 @@ private static String defaultValueNonOption(IJsonSchemaValidationProperties p, S
792816
if (p.getIsString()) {
793817
return "\"\"";
794818
}
795-
return fallbackDefaultValue;
796-
}
797-
798-
private static String defaultValueNonOption(CodegenProperty p) {
799-
if (p.getIsArray()) {
800-
return "Nil";
801-
}
802-
if (p.getIsMap()) {
803-
return "Map.empty";
804-
}
805-
if (p.isNumber || p.isNumeric) {
806-
return "0";
807-
}
808-
if (p.isBoolean) {
809-
return "false";
810-
}
811-
if (p.isUuid) {
812-
return "java.util.UUID.randomUUID()";
813-
}
814-
if (p.isModel) {
815-
return "null";
816-
}
817-
if (p.isDate || p.isDateTime) {
818-
return "null";
819-
}
820-
if (p.isString) {
821-
return "\"\"";
819+
if (fallbackDefaultValue != null && !fallbackDefaultValue.trim().isEmpty()) {
820+
return fallbackDefaultValue;
822821
}
823-
return p.defaultValue;
824-
}
825822

823+
return "null";
824+
}
826825

827826
@Override
828827
public CodegenProperty fromProperty(String name, Schema schema) {
@@ -847,9 +846,9 @@ public String getTypeDeclaration(Schema schema) {
847846

848847
@Override
849848
public String toModelImport(String name) {
850-
final String result = super.toModelImport(name);
849+
String result = super.toModelImport(name);
851850
if (importMapping.containsKey(name)) {
852-
return importMapping.get(name);
851+
result = importMapping.get(name);
853852
}
854853
return result;
855854
}

modules/openapi-generator/src/main/resources/scala-cask/apiPackage.mustache

+10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import java.time.LocalDate
1111
import java.util.UUID
1212
import scala.reflect.ClassTag
1313
import scala.util.*
14+
import upickle.default.*
1415

1516
// needed for BigDecimal params
1617
given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply)
@@ -142,6 +143,15 @@ extension (request: cask.Request) {
142143
def headerManyValues(paramName: String, required: Boolean): Parsed[List[String]] = Parsed.manyValues(request.headers, paramName, required)
143144

144145
def bodyAsString = new String(request.readAllBytes(), "UTF-8")
146+
147+
def bodyAsJson : Try[ujson.Value] = {
148+
val jason = bodyAsString
149+
try {
150+
Success(read[ujson.Value](jason))
151+
} catch {
152+
case scala.util.control.NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
153+
}
154+
}
145155

146156
def pathValue(index: Int, paramName: String, required : Boolean): Parsed[String] = {
147157
request

modules/openapi-generator/src/main/resources/scala-cask/apiRoutes.mustache

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class {{classname}}Routes(service : {{classname}}Service) extends cask.Routes {
4444

4545
val result = {{>parseHttpParams}}
4646

47-
result match {
47+
(result : @unchecked) match {
4848
case Left(error) => cask.Response(error, 500)
4949
{{#responses}}
5050
{{#dataType}}

modules/openapi-generator/src/main/resources/scala-cask/exampleApp.mustache

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//> using scala "3.3.1"
2-
//> using lib "com.lihaoyi::cask:0.9.2"
3-
//> using lib "com.lihaoyi::scalatags:0.8.2"
2+
//> using dep "com.lihaoyi::cask:0.9.2"
3+
//> using dep "com.lihaoyi::scalatags:0.8.2"
44
{{>licenseInfo}}
55

66
// this file was generated from app.mustache
@@ -11,11 +11,21 @@ package {{packageName}}
1111
import _root_.{{modelPackage}}.*
1212
import _root_.{{apiPackage}}.*
1313

14+
/** an example of how you can add your own additional routes to your app */
15+
object MoreRoutes extends cask.Routes {
16+
@cask.get("/echo")
17+
def more(request: cask.Request) = s"request was ${request.bodyAsString}"
18+
19+
initialize()
20+
}
21+
1422
/**
1523
* This is an example of how you might extends BaseApp for a runnable application.
1624
*
1725
* See the README.md for how to create your own app
1826
*/
1927
object ExampleApp extends BaseApp() {
28+
// override to include our additional route
29+
override def allRoutes = super.allRoutes ++ Option(MoreRoutes)
2030
start()
2131
}

modules/openapi-generator/src/main/resources/scala-cask/model.mustache

+9-7
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,28 @@ case class {{classname}}(
1717
/* {{{description}}} */
1818
{{/description}}
1919
{{name}}: {{#isEnum}}{{^required}}Option[{{/required}}{{classname}}.{{datatypeWithEnum}}{{^required}}]{{/required}}{{/isEnum}}{{^isEnum}}{{{vendorExtensions.x-datatype-model}}}{{/isEnum}}{{^required}} = {{{vendorExtensions.x-defaultValue-model}}} {{/required}}{{^-last}},{{/-last}}
20+
{{/vars}}
2021

21-
{{/vars}}) {
22+
{{#isAdditionalPropertiesTrue}}, additionalProperties : ujson.Value = ujson.Null{{/isAdditionalPropertiesTrue}}
23+
) {
2224
23-
def asJson: String = asData.asJson
25+
def asJsonString: String = asData.asJsonString
26+
def asJson: ujson.Value = asData.asJson
2427
2528
def asData : {{classname}}Data = {
2629
{{classname}}Data(
2730
{{#vars}}
2831
{{name}} = {{name}}{{#vendorExtensions.x-map-asModel}}.map(_.asData){{/vendorExtensions.x-map-asModel}}{{#vendorExtensions.x-wrap-in-optional}}.getOrElse({{{defaultValue}}}){{/vendorExtensions.x-wrap-in-optional}}{{^-last}},{{/-last}}
2932
{{/vars}}
33+
{{#isAdditionalPropertiesTrue}}, additionalProperties{{/isAdditionalPropertiesTrue}}
3034
)
3135
}
32-
3336
}
3437

35-
object {{classname}}{
36-
37-
given RW[{{classname}}] = {{classname}}Data.readWriter.bimap[{{classname}}](_.asData, _.asModel)
38+
object {{classname}} {
39+
given RW[{{classname}}] = summon[RW[ujson.Value]].bimap[{{classname}}](_.asJson, json => read[{{classname}}Data](json).asModel)
3840

39-
enum Fields(fieldName : String) extends Field(fieldName) {
41+
enum Fields(val fieldName : String) extends Field(fieldName) {
4042
{{#vars}}
4143
case {{name}} extends Fields("{{name}}")
4244
{{/vars}}

0 commit comments

Comments
 (0)