Skip to content
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
3 changes: 3 additions & 0 deletions .github/workflows/samples-go-client.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
- samples/client/others/go/allof_multiple_ref_and_discriminator/**
- samples/client/others/go/oneof-anyof-required/**
- samples/client/others/go/oneof-discriminator-lookup/**
- samples/client/others/go/oneof-not-enum/**
- samples/client/others/go/issue_20079_go_regex_wrongly_translated/**
pull_request:
paths:
Expand All @@ -18,6 +19,7 @@ on:
- samples/client/others/go/allof_multiple_ref_and_discriminator/**
- samples/client/others/go/oneof-anyof-required/**
- samples/client/others/go/oneof-discriminator-lookup/**
- samples/client/others/go/oneof-not-enum/**
- samples/client/others/go/issue_20079_go_regex_wrongly_translated/**
jobs:
build:
Expand All @@ -33,6 +35,7 @@ jobs:
- samples/client/others/go/allof_multiple_ref_and_discriminator/
- samples/client/others/go/oneof-anyof-required/
- samples/client/others/go/oneof-discriminator-lookup/
- samples/client/others/go/oneof-not-enum/
- samples/client/others/go/issue_20079_go_regex_wrongly_translated/
steps:
- uses: actions/checkout@v7
Expand Down
9 changes: 9 additions & 0 deletions bin/configs/go-oneof-not-enum.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
generatorName: go
outputDir: samples/client/others/go/oneof-not-enum
inputSpec: modules/openapi-generator/src/test/resources/3_1/go/oneof-not-enum.yaml
templateDir: modules/openapi-generator/src/test/resources/3_1/go
additionalProperties:
hideGenerationTimestamp: "true"
files:
oneof-not-enum_test.go:
destinationFilename: oneof-not-enum_test.go
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,147 @@ 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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added concise comments around the non-obvious parts of the new logic: scoping allowed-enum checks to the affected oneOf variants, handling inherited allOf properties, and matching the case-insensitive JSON field behavior used by encoding/json.

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

// Scope allowed-enum checks to oneOf variants whose sibling excludes values of the same 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;
}
// allOf children may need to validate properties inherited from their parent.
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)) {
// Required-key checks must match the case-insensitive field names accepted by encoding/json.
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;
}

/** Returns properties used for validation, including inherited allOf properties when present. */
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added Javadocs for both effectiveVars and stringEnumComparison, describing the effective/inherited property handling and the generated string/null enum comparison behavior.

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

/** Builds a Go comparison for supported string/null enum values, using equality in exclusion mode. */
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)
Comment thread
AndreyVMarkelov marked this conversation as resolved.
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