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
46 changes: 46 additions & 0 deletions .generator/schemas/v2/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4527,6 +4527,10 @@ components:
- ip_match
- "!ip_match"
- capture_data
- exists
- "!exists"
- equals
- "!equals"
example: "match_regex"
type: string
x-enum-varnames:
Expand All @@ -4541,6 +4545,10 @@ components:
- IP_MATCH
- NOT_IP_MATCH
- CAPTURE_DATA
- EXISTS
- NOT_EXISTS
- EQUALS
- NOT_EQUALS
ApplicationSecurityWafCustomRuleConditionOptions:
description: Options for the operator of this condition.
properties:
Expand Down Expand Up @@ -4581,6 +4589,8 @@ components:
description: "Regex to use with the condition. Only used with match_regex and !match_regex operator."
example: "path.*"
type: string
type:
$ref: "#/components/schemas/ApplicationSecurityWafCustomRuleConditionParametersType"
value:
description: |-
Store the captured value in the specified tag name. Only used with the capture_data operator.
Expand All @@ -4589,6 +4599,22 @@ components:
required:
- inputs
type: object
ApplicationSecurityWafCustomRuleConditionParametersType:
description: The type of the value to compare against. Only used with the equals and !equals operator.
enum:
- boolean
- signed
- unsigned
- float
- string
example: "string"
type: string
x-enum-varnames:
- BOOLEAN
- SIGNED
- UNSIGNED
- FLOAT
- STRING
ApplicationSecurityWafCustomRuleCreateAttributes:
description: "Create a new WAF custom rule."
properties:
Expand Down Expand Up @@ -18216,6 +18242,16 @@ components:
Must have HTTPS scheme and forwarding back to Datadog is not allowed.
example: https://example.com
type: string
sourcetype:
description: |-
The Splunk sourcetype for the events sent to this Splunk destination.

If absent, the default sourcetype `_json` is used. If set to null, the `sourcetype`
field is omitted from the Splunk HEC payload entirely. Otherwise, the provided string
value is used as the sourcetype.
example: my-source
nullable: true
type: string
type:
$ref: "#/components/schemas/CustomDestinationForwardDestinationSplunkType"
required:
Expand Down Expand Up @@ -18491,6 +18527,16 @@ components:
Must have HTTPS scheme and forwarding back to Datadog is not allowed.
example: https://example.com
type: string
sourcetype:
description: |-
The Splunk sourcetype for the events sent to this Splunk destination.

If absent, the default sourcetype `_json` is used. If set to null, the `sourcetype`
field is omitted from the Splunk HEC payload entirely. Otherwise, the provided string
value is used as the sourcetype.
example: my-source
nullable: true
type: string
type:
$ref: "#/components/schemas/CustomDestinationResponseForwardDestinationSplunkType"
required:
Expand Down
91 changes: 63 additions & 28 deletions .generator/src/generator/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,9 +559,18 @@ def format_data_with_schema_list(
nested_schema_name = "interface{}"
else:
if nested_schema_name:
nested_schema_name = f"{name_prefix}{nested_schema_name}"
elif list_schema.get("type") == "object" and list_schema.get("additionalProperties") == {}:
nested_schema_name = "map[string]interface{}"
additional = list_schema.get("additionalProperties", False)
if additional != False and not list_schema.get("properties"):
# Named schema is a map type (no struct generated for it) — use the
# map type directly to avoid referencing a non-existent struct.
value_type = (simple_type(additional) if isinstance(additional, dict) and additional else None) or "interface{}"
nested_schema_name = f"map[string]{value_type}"
else:
nested_schema_name = f"{name_prefix}{nested_schema_name}"
elif list_schema.get("additionalProperties", False) != False:
additional = list_schema.get("additionalProperties")
value_type = (simple_type(additional) if isinstance(additional, dict) and additional else None) or "interface{}"
nested_schema_name = f"map[string]{value_type}"
else:
nested_schema_name = "interface{}"

Expand Down Expand Up @@ -642,33 +651,66 @@ def format_data_with_schema_dict(
)
parameters += f"{camel_case(k)}: {value},\n"

if schema.get("additionalProperties"):
additional = schema.get("additionalProperties", False)
if additional != False:
saved_parameters = ""
if schema.get("properties"):
saved_parameters = parameters
parameters = ""
nested_schema = schema["additionalProperties"]
nested_schema_name = simple_type(nested_schema)
if not nested_schema_name:
nested_schema_name = schema_name(nested_schema)
if nested_schema_name:
nested_schema_name = name_prefix + nested_schema_name
elif nested_schema.get("type") is None:
nested_schema_name = "interface{}"
# Typed additionalProperties (non-empty dict): use it as the nested schema.
# Untyped (empty dict or True): any value is allowed, treat as interface{}.
nested_schema = additional if isinstance(additional, dict) and additional else None
if nested_schema:
nested_schema_name = simple_type(nested_schema)
if not nested_schema_name:
nested_schema_name = schema_name(nested_schema)
if nested_schema_name:
nested_schema_name = name_prefix + nested_schema_name
elif nested_schema.get("type") is None:
nested_schema_name = "interface{}"
else:
nested_schema_name = "interface{}"

has_properties = schema.get("properties")

for k, v in data.items():
if has_properties and k in schema["properties"]:
continue
value = format_data_with_schema(
v,
schema["additionalProperties"],
name_prefix=name_prefix,
replace_values=replace_values,
required=True,
**kwargs,
)
if nested_schema:
value = format_data_with_schema(
v,
nested_schema,
name_prefix=name_prefix,
replace_values=replace_values,
required=True,
**kwargs,
)
else:
# Infer schema from the Python value type so primitives are formatted
# correctly (e.g. strings with newlines use backtick literals, booleans
# emit "true"/"false"). bool must come before int because bool is a
# subclass of int in Python — format_interface would otherwise return
# "True"/"False" instead of valid Go literals.
# For complex types (dict, list) inferred stays {}: format_data_with_schema
# short-circuits on an empty schema and returns "", so the fallback
# f'"{v}"' emits a Python repr string. Not ideal but these values were
# previously silently dropped, so this is strictly an improvement.
if isinstance(v, bool):
inferred = {"type": "boolean"}
elif isinstance(v, (int, float)):
inferred = {"type": "number"}
elif isinstance(v, str):
inferred = {"type": "string"}
else:
inferred = {}
value = format_data_with_schema(
v,
inferred,
name_prefix=name_prefix,
replace_values=replace_values,
required=True,
**kwargs,
) or f'"{v}"'
parameters += f'"{k}": {value},\n'

# IMPROVE: find a better way to get nested schema name
Expand All @@ -687,14 +729,7 @@ def format_data_with_schema_dict(
return _format_oneof(schema, data, name, name_prefix, replace_values, required, nullable, **kwargs)

if schema.get("type") == "object" and "properties" not in schema:
if schema.get("additionalProperties") == {}:
name_prefix = ""
name = "map[string]interface{}"
reference = ""
for k, v in data.items():
parameters += f'"{k}": "{v}",\n'
else:
return "new(interface{})"
return "new(interface{})"

if not name:
raise ValueError(f"Unnamed schema {schema} for {data}")
Expand Down
85 changes: 85 additions & 0 deletions .generator/tests/test_formatter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# coding=utf-8
"""Unit tests for the formatter module."""

from generator.formatter import format_data_with_schema


class SchemaWithRef(dict):
"""A schema dict that carries a $ref, as the generator produces after resolving references."""

def __init__(self, *args, ref=None, **kwargs):
super().__init__(*args, **kwargs)
if ref:
self.__reference__ = {"$ref": ref}


class TestFormatDataWithSchemaAdditionalProperties:
"""Tests for format_data_with_schema with additionalProperties schemas."""

def test_empty_additional_properties_with_nullable_generates_map_literal(self):
"""additionalProperties: {} + nullable: true must not generate NewNullableXxx.

Before the fix, the Python truthiness of {} caused the additionalProperties
block to be skipped, falling through to the nullable path which generated
*NewNullableXxx(&Xxx{}) — a function that is never emitted by the model generator
for map schemas, causing a Go compile error.
"""
schema = SchemaWithRef(
{"nullable": True, "additionalProperties": {}},
ref="#/components/schemas/CustomFields",
)
result = format_data_with_schema({"key": "value"}, schema)
assert result == 'map[string]interface{}{\n"key": "value",\n}'
assert "NewNullable" not in result

def test_empty_additional_properties_with_type_object_generates_map_literal(self):
"""type: object + additionalProperties: {} + nullable: true must not generate NewNullableXxx."""
schema = SchemaWithRef(
{"type": "object", "nullable": True, "additionalProperties": {}},
ref="#/components/schemas/IncidentImpactFieldsObject",
)
result = format_data_with_schema({"field": "val"}, schema)
assert result == 'map[string]interface{}{\n"field": "val",\n}'
assert "NewNullable" not in result

def test_typed_additional_properties_generates_typed_map(self):
"""additionalProperties: {type: string} must generate map[string]string{} as before."""
schema = SchemaWithRef(
{"type": "object", "nullable": True, "additionalProperties": {"type": "string"}},
ref="#/components/schemas/StringMap",
)
result = format_data_with_schema({"k": "v"}, schema)
assert result == 'map[string]string{\n"k": "v",\n}'


class TestFormatDataWithSchemaArrayItems:
"""Tests for format_data_with_schema with array items that are named map schemas."""

def test_named_empty_additional_properties_as_array_item_generates_map_slice(self):
"""Array items that are named map schemas must generate []map[string]interface{}.

Before the fix, schema_name() returned "IDPConfigValueItem" for the item schema,
causing the formatter to emit []datadogV2.IDPConfigValueItem — a type that is never
generated for map schemas, causing a Go compile error.
"""
item_schema = SchemaWithRef(
{"type": "object", "additionalProperties": {}},
ref="#/components/schemas/IDPConfigValueItem",
)
array_schema = {"type": "array", "items": item_schema}
result = format_data_with_schema(
[{"id": "dashboard-1", "displayName": "My Dashboard"}], array_schema
)
assert "IDPConfigValueItem" not in result
assert "map[string]interface{}" in result

def test_named_typed_additional_properties_as_array_item_generates_typed_map_slice(self):
"""Array items that are named typed-map schemas must generate []map[string]string."""
item_schema = SchemaWithRef(
{"type": "object", "additionalProperties": {"type": "string"}},
ref="#/components/schemas/StringMapItem",
)
array_schema = {"type": "array", "items": item_schema}
result = format_data_with_schema([{"k": "v"}], array_schema)
assert "StringMapItem" not in result
assert "map[string]string" in result
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ const (
APPLICATIONSECURITYWAFCUSTOMRULECONDITIONOPERATOR_IP_MATCH ApplicationSecurityWafCustomRuleConditionOperator = "ip_match"
APPLICATIONSECURITYWAFCUSTOMRULECONDITIONOPERATOR_NOT_IP_MATCH ApplicationSecurityWafCustomRuleConditionOperator = "!ip_match"
APPLICATIONSECURITYWAFCUSTOMRULECONDITIONOPERATOR_CAPTURE_DATA ApplicationSecurityWafCustomRuleConditionOperator = "capture_data"
APPLICATIONSECURITYWAFCUSTOMRULECONDITIONOPERATOR_EXISTS ApplicationSecurityWafCustomRuleConditionOperator = "exists"
APPLICATIONSECURITYWAFCUSTOMRULECONDITIONOPERATOR_NOT_EXISTS ApplicationSecurityWafCustomRuleConditionOperator = "!exists"
APPLICATIONSECURITYWAFCUSTOMRULECONDITIONOPERATOR_EQUALS ApplicationSecurityWafCustomRuleConditionOperator = "equals"
APPLICATIONSECURITYWAFCUSTOMRULECONDITIONOPERATOR_NOT_EQUALS ApplicationSecurityWafCustomRuleConditionOperator = "!equals"
)

var allowedApplicationSecurityWafCustomRuleConditionOperatorEnumValues = []ApplicationSecurityWafCustomRuleConditionOperator{
Expand All @@ -40,6 +44,10 @@ var allowedApplicationSecurityWafCustomRuleConditionOperatorEnumValues = []Appli
APPLICATIONSECURITYWAFCUSTOMRULECONDITIONOPERATOR_IP_MATCH,
APPLICATIONSECURITYWAFCUSTOMRULECONDITIONOPERATOR_NOT_IP_MATCH,
APPLICATIONSECURITYWAFCUSTOMRULECONDITIONOPERATOR_CAPTURE_DATA,
APPLICATIONSECURITYWAFCUSTOMRULECONDITIONOPERATOR_EXISTS,
APPLICATIONSECURITYWAFCUSTOMRULECONDITIONOPERATOR_NOT_EXISTS,
APPLICATIONSECURITYWAFCUSTOMRULECONDITIONOPERATOR_EQUALS,
APPLICATIONSECURITYWAFCUSTOMRULECONDITIONOPERATOR_NOT_EQUALS,
}

// GetAllowedValues reeturns the list of possible values.
Expand Down
Loading
Loading