Skip to content

fix(ruby): use discriminators for oneOf [skip-bc] #4310

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 6, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.openapitools.codegen.*;
import org.openapitools.codegen.languages.RubyClientCodegen;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.ModelsMap;
import org.openapitools.codegen.model.OperationsMap;

public class AlgoliaRubyGenerator extends RubyClientCodegen {
Expand Down Expand Up @@ -80,6 +81,15 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
return Helpers.specifyCustomRequest(super.fromOperation(path, httpMethod, operation, servers));
}

@Override
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
Map<String, ModelsMap> models = super.postProcessAllModels(objs);
GenericPropagator.propagateGenericsToModels(models);
OneOf.updateModelsOneOf(models, modelPackage);
OneOf.addOneOfMetadata(models);
return models;
}

@Override
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> models) {
OperationsMap operations = super.postProcessOperationsWithModels(objs, models);
Expand Down
35 changes: 31 additions & 4 deletions playground/python/app/recommend.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,49 @@
from asyncio import run
from os import environ

from algoliasearch.recommend import __version__
from algoliasearch.recommend.client import RecommendClient
from algoliasearch.recommend.models.get_recommendations_params import (
GetRecommendationsParams,
)
from algoliasearch.recommend.models.related_query import RelatedQuery
from algoliasearch.recommend.models.recommendations_request import (
RecommendationsRequest,
)
from algoliasearch.recommend.models.related_model import RelatedModel

from dotenv import load_dotenv

load_dotenv("../.env")


async def main():
print("RecommendClient version", __version__)

client = RecommendClient("FOO", "BAR")
client = RecommendClient(
environ.get("ALGOLIA_APPLICATION_ID"), environ.get("ALGOLIA_ADMIN_KEY")
)

print("client initialized", client)

try:
response = await client.delete_recommend_rule(
index_name="nvim", model="test", object_id="objID"
response = await client.get_recommendations(
GetRecommendationsParams(
requests=[
RecommendationsRequest(
RelatedQuery(
index_name="cts_e2e_browse",
object_id="Æon Flux",
model=RelatedModel.RELATED_PRODUCTS,
threshold=30,
max_recommendations=2,
)
)
]
)
)

print(response)
print(len(response.results[0].hits), "recommendations found")
finally:
await client.close()

Expand Down
466 changes: 243 additions & 223 deletions playground/python/poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion playground/ruby/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: ../../clients/algoliasearch-client-ruby
specs:
algolia (3.10.1)
algolia (3.10.2)
base64 (>= 0.2.0, < 1)
faraday (>= 1.0.1, < 3.0)
faraday-net_http_persistent (>= 0.15, < 3)
Expand Down
19 changes: 19 additions & 0 deletions playground/ruby/recommend.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require 'dotenv'
require 'algolia'

Dotenv.load('../.env')

client = Algolia::RecommendClient.create(ENV['ALGOLIA_APPLICATION_ID'], ENV['ALGOLIA_ADMIN_KEY'])

res = client.get_recommendations(Algolia::Recommend::GetRecommendationsParams.new(
requests: [
Algolia::Recommend::RelatedQuery.new(
index_name: "cts_e2e_browse",
object_id: "Batman Dracula",
model: "related-products",
threshold: 0,
)
]
))

puts res
6 changes: 0 additions & 6 deletions templates/csharp/modelGeneric.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@
[ComVisible({{{vendorExtensions.x-com-visible}}})]
{{/vendorExtensions.x-com-visible}}
{{^useUnityWebRequest}}
{{#discriminator}}
[JsonConverter(typeof(JsonSubtypes), "{{{discriminatorName}}}")]
{{#mappedModels}}
[JsonSubtypes.KnownSubType(typeof({{{modelName}}}), "{{^vendorExtensions.x-discriminator-value}}{{{mappingName}}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{.}}}{{/vendorExtensions.x-discriminator-value}}")]
{{/mappedModels}}
{{/discriminator}}
{{/useUnityWebRequest}}
{{> visibility}} partial class {{classname}}{{#vendorExtensions.x-has-child-generic}}<T>{{/vendorExtensions.x-has-child-generic}}
{
Expand Down
29 changes: 0 additions & 29 deletions templates/go/model_anyof.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,6 @@ func (dst *{{classname}}) UnmarshalJSON(data []byte) error {
}

{{/isNullable}}
{{#discriminator}}
{{#mappedModels}}
{{#-first}}
// use discriminator value to speed up the lookup
var jsonDict map[string]any
err = json.Unmarshal(data, &jsonDict)
if err != nil {
return fmt.Errorf("Failed to unmarshal JSON into map for the discriminator lookup.")
}

{{/-first}}
// check if the discriminator value is '{{{mappingName}}}'
if jsonDict["{{{propertyBaseName}}}"] == "{{{mappingName}}}" {
// try to unmarshal JSON data into {{{modelName}}}
err = json.Unmarshal(data, &dst.{{{modelName}}});
if err == nil {
json{{{modelName}}}, _ := json.Marshal(dst.{{{modelName}}})
if string(json{{{modelName}}}) == "{}" { // empty struct
dst.{{{modelName}}} = nil
} else {
return nil // data stored in dst.{{{modelName}}}, return on the first match
}
} else {
dst.{{{modelName}}} = nil
}
}

{{/mappedModels}}
{{/discriminator}}
{{#anyOf}}
// try to unmarshal JSON data into {{{.}}}
err = json.Unmarshal(data, &dst.{{{.}}});
Expand Down
2 changes: 1 addition & 1 deletion templates/java/pojo.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class {{classname}}{{#vendorExtensions.x-has-child-generic}}<T>{{/vendorE
private {{> generic_type}} {{name}}{{#required}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/required}};
{{/isContainer}}
{{^isContainer}}
{{#isDiscriminator}}protected{{/isDiscriminator}}{{^isDiscriminator}}private{{/isDiscriminator}} {{{datatypeWithEnum}}} {{name}};
private {{{datatypeWithEnum}}} {{name}};
{{/isContainer}}

{{/vars}}
Expand Down
2 changes: 1 addition & 1 deletion templates/php/tests/requests/requests.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class {{clientPrefix}}Test extends TestCase implements HttpClientInterface

if (isset($request['body'])) {
$this->assertEquals(
json_encode($request['body']),
json_encode($request['body'], JSON_UNESCAPED_UNICODE),
$recordedRequest->getBody()->getContents()
);
}
Expand Down
11 changes: 7 additions & 4 deletions templates/ruby/partial_anyof_module.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@

SchemaMismatchError = Class.new(StandardError)

# Note: 'File' is missing here because in the regular case we get the data _after_ a call to JSON.parse.
def find_and_cast_into_type(klass, data)
return if data.nil?

Expand Down Expand Up @@ -77,12 +76,16 @@
if const.respond_to?(:openapi_any_of) # nested anyOf model
model = const.build(data)
return model if model
elsif const.respond_to?(:discriminator_attributes)
if const.discriminator_attributes().all? { |attr| data.key?(attr) }
model = const.build_from_hash(data)
end
else
# raise if data contains keys that are not known to the model
raise unless (data.keys - const.acceptable_attributes).empty?
# maybe it's an enum, or doens't have discriminators
model = const.build_from_hash(data)
return model if model
end

return model if model
end
end

Expand Down
26 changes: 9 additions & 17 deletions templates/ruby/partial_model_generic.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@
}
end

# Returns all the JSON keys this model knows about{{#parent}}, including the ones defined in its parent(s){{/parent}}
def self.acceptable_attributes
{{^parent}}
attribute_map.values
{{/parent}}
{{#parent}}
attribute_map.values.concat(superclass.acceptable_attributes)
{{/parent}}
{{#vendorExtensions.x-discriminator-fields.size}}
# Returns the keys that uniquely identify this oneOf variant when present
def self.discriminator_attributes
[
{{#vendorExtensions.x-discriminator-fields}}
:{{.}}{{^-last}},{{/-last}}
{{/vendorExtensions.x-discriminator-fields}}
]
end
{{/vendorExtensions.x-discriminator-fields.size}}

# Attribute type mapping.
def self.types_mapping
Expand Down Expand Up @@ -78,15 +79,6 @@

{{/-last}}
{{/allOf}}
{{#discriminator}}
{{#propertyName}}
# discriminator's property name in OpenAPI v3
def self.openapi_discriminator_name
:'{{{.}}}'
end

{{/propertyName}}
{{/discriminator}}
# Initializes the object
# @param [Hash] attributes Model attributes in the form of hash
def initialize(attributes = {})
Expand Down
53 changes: 5 additions & 48 deletions templates/ruby/partial_oneof_module.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -16,49 +16,10 @@

{{/-last}}
{{/oneOf}}
{{#discriminator}}
{{#propertyName}}
# Discriminator's property name (OpenAPI v3)
def openapi_discriminator_name
:'{{{.}}}'
end

{{/propertyName}}
{{#mappedModels}}
{{#-first}}
# Discriminator's mapping (OpenAPI v3)
def openapi_discriminator_mapping
{
{{/-first}}
:{{{mappingName}}} => :'{{{modelName}}}'{{^-last}},{{/-last}}
{{#-last}}
}
end

{{/-last}}
{{/mappedModels}}
{{/discriminator}}
# Builds the object
# @param [Mixed] Data to be matched against the list of oneOf items
# @return [Object] Returns the model or the data itself
def build(data)
{{#discriminator}}
discriminator_value = data[openapi_discriminator_name]
return nil if discriminator_value.nil?
{{#mappedModels}}
{{#-first}}

klass = openapi_discriminator_mapping[discriminator_value.to_s.to_sym]
return nil unless klass

{{moduleName}}::{{modelModule}}.const_get(klass).build_from_hash(data)
{{/-first}}
{{/mappedModels}}
{{^mappedModels}}
{{moduleName}}::{{modelModule}}.const_get(discriminator_value).build_from_hash(data)
{{/mappedModels}}
{{/discriminator}}
{{^discriminator}}
# Go through the list of oneOf items and attempt to identify the appropriate one.
# Note:
# - We do not attempt to check whether exactly one item matches.
Expand All @@ -76,15 +37,12 @@
end

openapi_one_of.include?(:AnyType) ? data : nil
{{/discriminator}}
end
{{^discriminator}}

private

SchemaMismatchError = Class.new(StandardError)

# Note: 'File' is missing here because in the regular case we get the data _after_ a call to JSON.parse.
def find_and_cast_into_type(klass, data)
return if data.nil?

Expand Down Expand Up @@ -119,12 +77,12 @@
if const.respond_to?(:openapi_one_of)
# nested oneOf model
model = const.build(data)
elsif const.respond_to?(:acceptable_attributes)
# raise if data contains keys that are not known to the model
raise unless (data.keys - const.acceptable_attributes).empty?
model = const.build_from_hash(data)
elsif const.respond_to?(:discriminator_attributes)
if const.discriminator_attributes().all? { |attr| data.key?(attr) }
model = const.build_from_hash(data)
end
else
# maybe it's an enum
# maybe it's an enum, or doens't have discriminators
model = const.build_from_hash(data)
end

Expand All @@ -136,6 +94,5 @@
rescue
raise SchemaMismatchError, "#{data} doesn't match the #{klass} type"
end
{{/discriminator}}
end
end
7 changes: 7 additions & 0 deletions templates/scala/oneof_trait.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ object {{classname}}Serializer extends Serializer[{{classname}}] {
{{#isModel}}
case value: JObject {{#vendorExtensions.x-discriminator-fields}}{{#-first}}if{{/-first}}{{^-first}}&&{{/-first}} value.obj.exists(_._1 == "{{{.}}}"){{/vendorExtensions.x-discriminator-fields}} => Extraction.extract[{{{datatypeWithEnum}}}](value)
{{/isModel}}
{{^isModel}}
{{^isEnumRef}}
{{#vendorExtensions.x-discriminator-fields.size}}
case value: JObject {{#vendorExtensions.x-discriminator-fields}}{{#-first}}if{{/-first}}{{^-first}}&&{{/-first}} value.obj.exists(_._1 == "{{{.}}}"){{/vendorExtensions.x-discriminator-fields}} => Extraction.extract[{{{datatypeWithEnum}}}](value)
{{/vendorExtensions.x-discriminator-fields.size}}
{{/isEnumRef}}
{{/isModel}}
{{#isMap}}
case value: JObject => {{classname}}.apply(Extraction.extract[{{{datatypeWithEnum}}}](value))
{{/isMap}}
Expand Down
Loading