Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.openapitools.codegen.languages;

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.collect.Iterables;
import com.samskivert.mustache.Mustache;
import io.swagger.v3.oas.models.media.Schema;
Expand Down Expand Up @@ -600,6 +601,143 @@ public ModelsMap postProcessModels(ModelsMap objs) {
return objs;
}

@Override

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion would be nice to add more comments in the new code block (line 606 - 708)

public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
objs = super.postProcessAllModels(objs);
if (!generateUnmarshalJSON) {
return objs;
}

// An allowed enum only needs strict validation when a oneOf sibling excludes values for that property.
Map<CodegenModel, Set<String>> allowedOneOfProperties = new IdentityHashMap<>();
for (ModelsMap models : objs.values()) {
for (ModelMap modelMap : models.getModels()) {
CodegenModel union = modelMap.getModel();
for (String excludedMemberName : union.oneOf) {
CodegenModel excludedMember = ModelUtils.getModelByName(excludedMemberName, objs);
if (excludedMember == null) {
continue;
}
for (CodegenProperty excludedProperty : effectiveVars(excludedMember).values()) {
CodegenProperty not = excludedProperty.getComposedSchemas() == null
? null : excludedProperty.getComposedSchemas().getNot();
if (stringEnumComparison(not, true) == null) {
continue;
}
for (String allowedMemberName : union.oneOf) {
if (allowedMemberName.equals(excludedMemberName)) {
continue;
}
CodegenModel allowedMember = ModelUtils.getModelByName(allowedMemberName, objs);
if (allowedMember == null) {
continue;
}
CodegenProperty allowedProperty = effectiveVars(allowedMember).get(excludedProperty.baseName);
if (stringEnumComparison(allowedProperty, false) != null) {
allowedOneOfProperties.computeIfAbsent(allowedMember, ignored -> new HashSet<>())
.add(excludedProperty.baseName);
}
}
}
}
}
}

for (ModelsMap models : objs.values()) {
for (ModelMap modelMap : models.getModels()) {
CodegenModel model = modelMap.getModel();
if (model.isEnum || hasOneOf(model) || hasAnyOf(model)) {
continue;
}
Map<String, CodegenProperty> effectiveVars = effectiveVars(model);
Map<String, CodegenProperty> ownVars = new HashMap<>();
for (CodegenProperty param : model.vars) {
ownVars.put(param.baseName, param);
}
List<CodegenProperty> validationVars = new ArrayList<>();
boolean hasInheritedStringEnumValidation = false;
for (CodegenProperty param : effectiveVars.values()) {
String allowed = allowedOneOfProperties.getOrDefault(model, Collections.emptySet()).contains(param.baseName)
? stringEnumComparison(param, false) : null;
CodegenProperty not = param.getComposedSchemas() == null ? null : param.getComposedSchemas().getNot();
String excluded = stringEnumComparison(not, true);
if (allowed != null || excluded != null) {
hasInheritedStringEnumValidation |= !ownVars.containsKey(param.baseName);
if (ownVars.containsKey(param.baseName) && ownVars.get(param.baseName).vendorExtensions.containsKey("x-go-datatag")
&& !param.vendorExtensions.containsKey("x-go-datatag")) {
param.vendorExtensions.put("x-go-datatag", ownVars.get(param.baseName).vendorExtensions.get("x-go-datatag"));
}
validationVars.add(param);
param.vendorExtensions.put("x-go-enum-property-name", TextNode.valueOf(param.baseName).toString());
if (allowed != null) {
param.vendorExtensions.put("x-go-allowed-string-enum-comparison", allowed);
if (param.isNullable && (param.isEnumRef || ((List<?>) param.allowableValues.get("values")).contains(null))) {
param.vendorExtensions.put("x-go-allowed-string-enum-null", true);
}
}
if (excluded != null) {
param.vendorExtensions.put("x-go-excluded-string-enum-comparison", excluded);
}
}
}
if (!validationVars.isEmpty()) {
model.vendorExtensions.put("x-go-has-string-enum-validation", true);
model.vendorExtensions.put("x-go-string-enum-validation-vars", validationVars);
List<Map<String, String>> imports = models.getImports();
if (imports.stream().noneMatch(i -> "fmt".equals(i.get("import")))) {
imports.add(createMapping("import", "fmt"));
}
if (model.hasRequired && validationVars.stream().anyMatch(param -> param.required)) {
model.vendorExtensions.put("x-go-enum-required-case-fold", true);
if (imports.stream().noneMatch(i -> "strings".equals(i.get("import")))) {
imports.add(createMapping("import", "strings"));
}
}
imports.sort(Comparator.comparing(i -> i.get("import")));
}
if (hasInheritedStringEnumValidation && !model.isAdditionalPropertiesTrue) {
for (CodegenProperty param : effectiveVars.values()) {
param.vendorExtensions.put("x-go-flattened-json-name", TextNode.valueOf(param.baseName + (param.required ? "" : ",omitempty")).toString());
}
model.vendorExtensions.put("x-go-inherited-string-enum-validation", true);
model.vendorExtensions.put("x-go-flattened-vars", new ArrayList<>(effectiveVars.values()));
}
}
}
return objs;
}

private static Map<String, CodegenProperty> effectiveVars(CodegenModel model) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add docstrings to newly-created functions effectiveVars, stringEnumComparison

Map<String, CodegenProperty> vars = new LinkedHashMap<>();
for (CodegenProperty param : model.parent == null ? model.vars : model.allVars) {
vars.put(param.baseName, param);
}
return vars;
}

private static String stringEnumComparison(CodegenProperty property, boolean excluded) {
if (property == null || !(property.isString && property.isEnum || property.isEnumRef)
|| property.allowableValues == null
|| !(property.allowableValues.get("values") instanceof List)) {
return null;
}
StringJoiner comparisons = new StringJoiner(excluded ? " || " : " && ");
for (Object value : (List<?>) property.allowableValues.get("values")) {
if (value == null && excluded) {
comparisons.add("value == nil");
continue;
}
if (value == null && property.isNullable) {
continue;
}
if (!(value instanceof String)) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
return null;
}
comparisons.add("value " + (excluded ? "==" : "!=") + " " + TextNode.valueOf((String) value));
}
return comparisons.length() == 0 ? (property.isNullable && !excluded ? "true" : null) : comparisons.toString();
}

/**
* Prefixes the generated {@code unknown_default_open_api} enum case with the model name when enum class prefixing
* is disabled.
Expand Down
150 changes: 150 additions & 0 deletions modules/openapi-generator/src/main/resources/go/model_simple.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,89 @@ type _{{{classname}}} {{{classname}}}

{{/hasRequired}}
{{/isAdditionalPropertiesTrue}}
{{#vendorExtensions.x-go-has-string-enum-validation}}
func (o *{{{classname}}}) validateStringEnumValues(data []byte) error {
var fields struct {
{{#vendorExtensions.x-go-string-enum-validation-vars}}
{{name}} json.RawMessage{{#vendorExtensions.x-go-datatag}}{{{.}}}{{/vendorExtensions.x-go-datatag}}{{^vendorExtensions.x-go-datatag}} `json:{{{vendorExtensions.x-go-enum-property-name}}}`{{/vendorExtensions.x-go-datatag}}
{{/vendorExtensions.x-go-string-enum-validation-vars}}
}
if err := json.Unmarshal(data, &fields); err != nil {
return err
}
{{#vendorExtensions.x-go-string-enum-validation-vars}}
{{#vendorExtensions.x-go-allowed-string-enum-comparison}}
if raw := fields.{{name}}; raw != nil {
{{^vendorExtensions.x-go-allowed-string-enum-null}}
if string(raw) == "null" {
return fmt.Errorf("invalid null value for property %s", {{{vendorExtensions.x-go-enum-property-name}}})
}
{{/vendorExtensions.x-go-allowed-string-enum-null}}
var value string
if err := json.Unmarshal(raw, &value); err != nil {
return err
}
if {{#vendorExtensions.x-go-allowed-string-enum-null}}string(raw) != "null" && ({{/vendorExtensions.x-go-allowed-string-enum-null}}{{{vendorExtensions.x-go-allowed-string-enum-comparison}}}{{#vendorExtensions.x-go-allowed-string-enum-null}}){{/vendorExtensions.x-go-allowed-string-enum-null}} {
return fmt.Errorf("invalid value %q for property %s", value, {{{vendorExtensions.x-go-enum-property-name}}})
}
}
{{/vendorExtensions.x-go-allowed-string-enum-comparison}}
{{#vendorExtensions.x-go-excluded-string-enum-comparison}}
if raw := fields.{{name}}; raw != nil {
var value interface{}
if err := json.Unmarshal(raw, &value); err != nil {
return err
}
if {{{vendorExtensions.x-go-excluded-string-enum-comparison}}} {
return fmt.Errorf("excluded value %v for property %s", value, {{{vendorExtensions.x-go-enum-property-name}}})
}
}
{{/vendorExtensions.x-go-excluded-string-enum-comparison}}
{{/vendorExtensions.x-go-string-enum-validation-vars}}
return nil
}

{{^isAdditionalPropertiesTrue}}
{{^hasRequired}}
type _{{{classname}}} {{{classname}}}

func (o *{{{classname}}}) UnmarshalJSON(data []byte) error {
if err := o.validateStringEnumValues(data); err != nil {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
return err
}
{{#vendorExtensions.x-go-inherited-string-enum-validation}}
var decoded struct {
{{#vendorExtensions.x-go-flattened-vars}}
{{^-first}}

{{/-first}}
{{name}} {{^required}}{{^isNullable}}{{^isArray}}{{^isFreeFormObject}}*{{/isFreeFormObject}}{{/isArray}}{{/isNullable}}{{/required}}{{{dataType}}}{{#vendorExtensions.x-go-datatag}}{{{.}}}{{/vendorExtensions.x-go-datatag}}{{^vendorExtensions.x-go-datatag}} `json:{{{vendorExtensions.x-go-flattened-json-name}}}`{{/vendorExtensions.x-go-datatag}}
{{/vendorExtensions.x-go-flattened-vars}}
}
{{#vendorExtensions.x-go-flattened-vars}}
decoded.{{name}} = o.{{name}}
{{/vendorExtensions.x-go-flattened-vars}}
if err := json.Unmarshal(data, &decoded); err != nil {
return err
}
*o = {{{classname}}}{}
{{#vendorExtensions.x-go-flattened-vars}}
o.{{name}} = decoded.{{name}}
{{/vendorExtensions.x-go-flattened-vars}}
{{/vendorExtensions.x-go-inherited-string-enum-validation}}
{{^vendorExtensions.x-go-inherited-string-enum-validation}}
value := _{{{classname}}}(*o)
if err := json.Unmarshal(data, &value); err != nil {
return err
}
*o = {{{classname}}}(value)
{{/vendorExtensions.x-go-inherited-string-enum-validation}}
return nil
}

{{/hasRequired}}
{{/isAdditionalPropertiesTrue}}
{{/vendorExtensions.x-go-has-string-enum-validation}}
// New{{classname}} instantiates a new {{classname}} object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
Expand Down Expand Up @@ -360,10 +443,20 @@ func (o {{classname}}) ToMap() (map[string]interface{}, error) {
{{#vendorExtensions.x-go-generate-unmarshal-json}}
{{#isAdditionalPropertiesTrue}}
func (o *{{{classname}}}) UnmarshalJSON(data []byte) (err error) {
{{#vendorExtensions.x-go-has-string-enum-validation}}
if err := o.validateStringEnumValues(data); err != nil {
return err
}
{{/vendorExtensions.x-go-has-string-enum-validation}}
{{/isAdditionalPropertiesTrue}}
{{^isAdditionalPropertiesTrue}}
{{#hasRequired}}
func (o *{{{classname}}}) UnmarshalJSON(data []byte) (err error) {
{{#vendorExtensions.x-go-has-string-enum-validation}}
if err := o.validateStringEnumValues(data); err != nil {
return err
}
{{/vendorExtensions.x-go-has-string-enum-validation}}
{{/hasRequired}}
{{/isAdditionalPropertiesTrue}}
{{#hasRequired}}
Expand Down Expand Up @@ -398,18 +491,51 @@ func (o *{{{classname}}}) UnmarshalJSON(data []byte) (err error) {
return err;
}

{{#vendorExtensions.x-go-enum-required-case-fold}}
lookupRequiredProperty := func(requiredProperty string) (interface{}, string, bool) {
if value, exists := allProperties[requiredProperty]; exists {
return value, requiredProperty, true
}
for name, value := range allProperties {
if strings.EqualFold(name, requiredProperty) {
return value, name, true
}
}
return nil, "", false
}
{{/vendorExtensions.x-go-enum-required-case-fold}}
for _, requiredProperty := range(requiredProperties) {
{{#useDefaultValuesForRequiredVars}}
{{#vendorExtensions.x-go-enum-required-case-fold}}
if value, matchedKey, exists := lookupRequiredProperty(requiredProperty); !exists || value == "" {
{{/vendorExtensions.x-go-enum-required-case-fold}}
{{^vendorExtensions.x-go-enum-required-case-fold}}
if value, exists := allProperties[requiredProperty]; !exists || value == "" {
{{/vendorExtensions.x-go-enum-required-case-fold}}
if _, ok := defaultValueFuncMap[requiredProperty]; ok {
{{#vendorExtensions.x-go-enum-required-case-fold}}
if exists && matchedKey != requiredProperty {
delete(allProperties, matchedKey)
}
{{/vendorExtensions.x-go-enum-required-case-fold}}
allProperties[requiredProperty] = defaultValueFuncMap[requiredProperty]()
defaultValueApplied = true
}
}
{{#vendorExtensions.x-go-enum-required-case-fold}}
if value, _, exists := lookupRequiredProperty(requiredProperty); !exists || value == ""{
{{/vendorExtensions.x-go-enum-required-case-fold}}
{{^vendorExtensions.x-go-enum-required-case-fold}}
if value, exists := allProperties[requiredProperty]; !exists || value == ""{
{{/vendorExtensions.x-go-enum-required-case-fold}}
{{/useDefaultValuesForRequiredVars}}
{{^useDefaultValuesForRequiredVars}}
{{#vendorExtensions.x-go-enum-required-case-fold}}
if _, _, exists := lookupRequiredProperty(requiredProperty); !exists {
{{/vendorExtensions.x-go-enum-required-case-fold}}
{{^vendorExtensions.x-go-enum-required-case-fold}}
if _, exists := allProperties[requiredProperty]; !exists {
{{/vendorExtensions.x-go-enum-required-case-fold}}
{{/useDefaultValuesForRequiredVars}}
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
Expand Down Expand Up @@ -545,6 +671,29 @@ func (o *{{{classname}}}) UnmarshalJSON(data []byte) (err error) {
{{/isAdditionalPropertiesTrue}}
{{^isAdditionalPropertiesTrue}}
{{#hasRequired}}
{{#vendorExtensions.x-go-inherited-string-enum-validation}}
var decoded struct {
{{#vendorExtensions.x-go-flattened-vars}}
{{^-first}}

{{/-first}}
{{name}} {{^required}}{{^isNullable}}{{^isArray}}{{^isFreeFormObject}}*{{/isFreeFormObject}}{{/isArray}}{{/isNullable}}{{/required}}{{{dataType}}}{{#vendorExtensions.x-go-datatag}}{{{.}}}{{/vendorExtensions.x-go-datatag}}{{^vendorExtensions.x-go-datatag}} `json:{{{vendorExtensions.x-go-flattened-json-name}}}`{{/vendorExtensions.x-go-datatag}}
{{/vendorExtensions.x-go-flattened-vars}}
}

decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err = decoder.Decode(&decoded); err != nil {
return err
}

*o = {{{classname}}}{}
{{#vendorExtensions.x-go-flattened-vars}}
o.{{name}} = decoded.{{name}}
{{/vendorExtensions.x-go-flattened-vars}}
return nil
{{/vendorExtensions.x-go-inherited-string-enum-validation}}
{{^vendorExtensions.x-go-inherited-string-enum-validation}}
var{{{classname}}} := _{{{classname}}}{}

decoder := json.NewDecoder(bytes.NewReader(data))
Expand All @@ -558,6 +707,7 @@ func (o *{{{classname}}}) UnmarshalJSON(data []byte) (err error) {
*o = {{{classname}}}(var{{{classname}}})

return err
{{/vendorExtensions.x-go-inherited-string-enum-validation}}
}

{{/hasRequired}}
Expand Down
Loading
Loading