Skip to content

Commit

Permalink
Improve resiliency of Ruby SDK when deserializing enums/oneOfs (#460)
Browse files Browse the repository at this point in the history
* port synthetics replay-only test to ruby

* add test files

* test templates

* fix replay-only mode

* fix tests

* update tests to assert for _has_unparsed attribute

* apply code review suggestions

* do not delete cassettes if replay only

* fix env var checking

* re-add conditional to skip cassette deletion on replay-only scenarios

* Regenerate client from commit 20a3ffe of spec repo

Co-authored-by: Sherzod K <sherzod.karimov@datadoghq.com>
Co-authored-by: api-clients-generation-pipeline[bot] <54105614+api-clients-generation-pipeline[bot]@users.noreply.github.com>
Co-authored-by: ci.datadog-api-spec <packages@datadoghq.com>
  • Loading branch information
3 people authored Aug 17, 2021
1 parent 63f895e commit b4b8bbb
Show file tree
Hide file tree
Showing 945 changed files with 6,506 additions and 1,154 deletions.
8 changes: 4 additions & 4 deletions .apigentools-info
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
"spec_versions": {
"v1": {
"apigentools_version": "1.4.1.dev11",
"regenerated": "2021-08-16 14:47:31.471744",
"spec_repo_commit": "e3699c0"
"regenerated": "2021-08-17 12:42:09.770802",
"spec_repo_commit": "20a3ffe"
},
"v2": {
"apigentools_version": "1.4.1.dev11",
"regenerated": "2021-08-16 14:48:18.521900",
"spec_repo_commit": "e3699c0"
"regenerated": "2021-08-17 12:42:49.974175",
"spec_repo_commit": "20a3ffe"
}
}
}
6 changes: 5 additions & 1 deletion .generator/templates/base_object.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@
else # model
# models (e.g. Pet) or oneOf
klass = {{moduleName}}.const_get(type)
klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
res = klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
if res.instance_of? {{moduleName}}::UnparsedObject
self._unparsed = true
end
res
end
end

Expand Down
14 changes: 14 additions & 0 deletions .generator/templates/configuration.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -370,4 +370,18 @@ module {{moduleName}}
url
end
end

class UnparsedObject
# Defines unparsed object
attr_accessor :data

def initialize(data)
@data = data
end

def to_hash
self.data
end
end

end
3 changes: 1 addition & 2 deletions .generator/templates/partial_model_enum_class.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
# @return [String] The enum value
def build_from_hash(value)
constantValues = {{classname}}.constants.select { |c| {{classname}}::const_get(c) == value }
raise "Invalid ENUM value #{value} for class #{{{classname}}}" if constantValues.empty?
value
constantValues.empty? ? {{moduleName}}::UnparsedObject.new(value) : value
end
end
3 changes: 3 additions & 0 deletions .generator/templates/partial_model_generic.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
# {{{description}}}
{{/description}}
class {{classname}}{{#parent}} < {{{.}}}{{/parent}}
# whether the object has unparsed attributes
attr_accessor :_unparsed

{{#vars}}
{{#description}}
# {{{description}}}
Expand Down
11 changes: 10 additions & 1 deletion .generator/templates/partial_oneof_module.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
{{/description}}
module {{classname}}
class << self
attr_accessor :_unparsed

{{#oneOf}}
{{#-first}}
# List of class defined in oneOf (OpenAPI v3)
Expand Down Expand Up @@ -70,12 +72,19 @@
begin
next if klass == :AnyType # "nullable: true"
typed_data = find_and_cast_into_type(klass, data)
next if typed_data._unparsed
return typed_data if typed_data
rescue # rescue all errors so we keep iterating even if the current item lookup raises
end
end

openapi_one_of.include?(:AnyType) ? data : nil
if openapi_one_of.include?(:AnyType)
data
else
self._unparsed = true
{{moduleName}}::UnparsedObject.new(data)
end

{{/discriminator}}
end
{{^discriminator}}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion features/support/hooks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
skip_this_scenario('skip ruby')
end

Before('@replay-only') do |scenario|
skip_this_scenario('replay only') unless ENV["RECORD"].nil? || ENV["RECORD"] == "false"
end

Around do |scenario, block|
current_span = Datadog.configuration[:cucumber][:tracer].active_span
unless current_span.nil?
Expand All @@ -27,7 +31,7 @@

Around do |scenario, block|
VCR.use_cassette(scenario.location.file.chomp('.feature') + "/" + scenario.name.gsub(/[^A-Za-z0-9]+/, '-')[0..100], :record_on_error => false, :match_requests_on => [:method, :host, :path, :query, :body_as_json]) do |cassette|
File.delete(cassette.file) if ENV["RECORD"] == "true" && File.exist?(cassette.file)
File.delete(cassette.file) if ENV["RECORD"] == "true" && File.exist?(cassette.file) && !scenario.match_tags?("@replay-only")
Timecop.freeze(use_real_time? ? Time.now : cassette.originally_recorded_at) do
block.call
end
Expand Down
2 changes: 1 addition & 1 deletion features/v1/synthetics.feature
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Feature: Synthetics
And a valid "appKeyAuth" key in the system
And an instance of "Synthetics" API

@replay-only @skip-java @skip-ruby
@replay-only @skip-java
Scenario: Client is resilient to enum and oneOf deserialization errors
Given new "ListTests" request
When the request is sent
Expand Down
14 changes: 14 additions & 0 deletions lib/datadog_api_client/v1/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -457,4 +457,18 @@ def server_url(index, variables = {}, servers = nil)
url
end
end

class UnparsedObject
# Defines unparsed object
attr_accessor :data

def initialize(data)
@data = data
end

def to_hash
self.data
end
end

end
3 changes: 1 addition & 2 deletions lib/datadog_api_client/v1/models/access_role.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ def self.build_from_hash(value)
# @return [String] The enum value
def build_from_hash(value)
constantValues = AccessRole.constants.select { |c| AccessRole::const_get(c) == value }
raise "Invalid ENUM value #{value} for class #AccessRole" if constantValues.empty?
value
constantValues.empty? ? DatadogAPIClient::V1::UnparsedObject.new(value) : value
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
module DatadogAPIClient::V1
# Alert graphs are timeseries graphs showing the current status of any monitor defined on your system.
class AlertGraphWidgetDefinition
# whether the object has unparsed attributes
attr_accessor :_unparsed

# ID of the alert to use in the widget.
attr_accessor :alert_id

Expand Down Expand Up @@ -244,7 +247,11 @@ def _deserialize(type, value)
else # model
# models (e.g. Pet) or oneOf
klass = DatadogAPIClient::V1.const_get(type)
klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
res = klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
if res.instance_of? DatadogAPIClient::V1::UnparsedObject
self._unparsed = true
end
res
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ def self.build_from_hash(value)
# @return [String] The enum value
def build_from_hash(value)
constantValues = AlertGraphWidgetDefinitionType.constants.select { |c| AlertGraphWidgetDefinitionType::const_get(c) == value }
raise "Invalid ENUM value #{value} for class #AlertGraphWidgetDefinitionType" if constantValues.empty?
value
constantValues.empty? ? DatadogAPIClient::V1::UnparsedObject.new(value) : value
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
module DatadogAPIClient::V1
# Alert values are query values showing the current value of the metric in any monitor defined on your system.
class AlertValueWidgetDefinition
# whether the object has unparsed attributes
attr_accessor :_unparsed

# ID of the alert to use in the widget.
attr_accessor :alert_id

Expand Down Expand Up @@ -250,7 +253,11 @@ def _deserialize(type, value)
else # model
# models (e.g. Pet) or oneOf
klass = DatadogAPIClient::V1.const_get(type)
klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
res = klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
if res.instance_of? DatadogAPIClient::V1::UnparsedObject
self._unparsed = true
end
res
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ def self.build_from_hash(value)
# @return [String] The enum value
def build_from_hash(value)
constantValues = AlertValueWidgetDefinitionType.constants.select { |c| AlertValueWidgetDefinitionType::const_get(c) == value }
raise "Invalid ENUM value #{value} for class #AlertValueWidgetDefinitionType" if constantValues.empty?
value
constantValues.empty? ? DatadogAPIClient::V1::UnparsedObject.new(value) : value
end
end
end
9 changes: 8 additions & 1 deletion lib/datadog_api_client/v1/models/api_error_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
module DatadogAPIClient::V1
# Error response object.
class APIErrorResponse
# whether the object has unparsed attributes
attr_accessor :_unparsed

# Array of errors returned by the API.
attr_accessor :errors

Expand Down Expand Up @@ -178,7 +181,11 @@ def _deserialize(type, value)
else # model
# models (e.g. Pet) or oneOf
klass = DatadogAPIClient::V1.const_get(type)
klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
res = klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
if res.instance_of? DatadogAPIClient::V1::UnparsedObject
self._unparsed = true
end
res
end
end

Expand Down
9 changes: 8 additions & 1 deletion lib/datadog_api_client/v1/models/api_key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
module DatadogAPIClient::V1
# Datadog API key.
class ApiKey
# whether the object has unparsed attributes
attr_accessor :_unparsed

# Date of creation of the API key.
attr_accessor :created

Expand Down Expand Up @@ -225,7 +228,11 @@ def _deserialize(type, value)
else # model
# models (e.g. Pet) or oneOf
klass = DatadogAPIClient::V1.const_get(type)
klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
res = klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
if res.instance_of? DatadogAPIClient::V1::UnparsedObject
self._unparsed = true
end
res
end
end

Expand Down
9 changes: 8 additions & 1 deletion lib/datadog_api_client/v1/models/api_key_list_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
module DatadogAPIClient::V1
# List of API and application keys available for a given organization.
class ApiKeyListResponse
# whether the object has unparsed attributes
attr_accessor :_unparsed

# Array of API keys.
attr_accessor :api_keys

Expand Down Expand Up @@ -173,7 +176,11 @@ def _deserialize(type, value)
else # model
# models (e.g. Pet) or oneOf
klass = DatadogAPIClient::V1.const_get(type)
klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
res = klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
if res.instance_of? DatadogAPIClient::V1::UnparsedObject
self._unparsed = true
end
res
end
end

Expand Down
9 changes: 8 additions & 1 deletion lib/datadog_api_client/v1/models/api_key_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
module DatadogAPIClient::V1
# An API key with its associated metadata.
class ApiKeyResponse
# whether the object has unparsed attributes
attr_accessor :_unparsed

attr_accessor :api_key

# Attribute mapping from ruby-style variable name to JSON key.
Expand Down Expand Up @@ -170,7 +173,11 @@ def _deserialize(type, value)
else # model
# models (e.g. Pet) or oneOf
klass = DatadogAPIClient::V1.const_get(type)
klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
res = klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
if res.instance_of? DatadogAPIClient::V1::UnparsedObject
self._unparsed = true
end
res
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
module DatadogAPIClient::V1
# Column properties.
class ApmStatsQueryColumnType
# whether the object has unparsed attributes
attr_accessor :_unparsed

# A user-assigned alias for the column.
attr_accessor :_alias

Expand Down Expand Up @@ -204,7 +207,11 @@ def _deserialize(type, value)
else # model
# models (e.g. Pet) or oneOf
klass = DatadogAPIClient::V1.const_get(type)
klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
res = klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
if res.instance_of? DatadogAPIClient::V1::UnparsedObject
self._unparsed = true
end
res
end
end

Expand Down
Loading

0 comments on commit b4b8bbb

Please sign in to comment.