From fbdbbc6772f20612cb501412ad35ecb8a8affc16 Mon Sep 17 00:00:00 2001 From: AxelRay <110029405+axelray-dev@users.noreply.github.com> Date: Wed, 23 Sep 2026 23:14:44 +0800 Subject: [PATCH 1/4] fix(ruby-nextgen): expose nested resources through clients --- .../languages/RubyNextgenClientCodegen.java | 122 ++++++++++++++++++ .../ruby-nextgen/api_operations.mustache | 24 +++- .../resources/ruby-nextgen/api_test.mustache | 16 +++ .../RubyNextgenClientCodegenTest.java | 32 +++++ .../3_0/ruby-nextgen/nested-resources.yaml | 74 +++++++++++ .../ruby-nextgen/lib/petstore/api/store.rb | 4 + .../ruby-nextgen/spec/api/store_spec.rb | 4 + 7 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/ruby-nextgen/nested-resources.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyNextgenClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyNextgenClientCodegen.java index 2cddf09f729b..663eed635ddd 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyNextgenClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyNextgenClientCodegen.java @@ -91,6 +91,8 @@ public class RubyNextgenClientCodegen extends AbstractRubyCodegen { private Set resourceSegments = Collections.emptySet(); private String apiBasePrefix = ""; private int emptyMethodNameCounter = 0; + private List> rubyNamespaces = Collections.emptyList(); + private final Set namespaceOnlyApiTags = new HashSet<>(); // Accumulated across postProcessModels calls: file basename -> class name, for every // autoloaded model. Consumed by postProcessSupportingFileData to emit Zeitwerk @@ -531,12 +533,112 @@ public void preprocessOpenAPI(OpenAPI openAPI) { this.apiBasePrefix = additionalProperties.containsKey("apiBasePath") ? stripSlashes((String) additionalProperties.get("apiBasePath")) : RubyApiRouting.commonBasePrefix(paths); + this.rubyNamespaces = buildRubyNamespaces(openAPI); + this.namespaceOnlyApiTags.clear(); + for (Map namespace : rubyNamespaces) { + if (!Boolean.TRUE.equals(namespace.get("hasDirectOperations"))) { + @SuppressWarnings("unchecked") + List> resources = (List>) namespace.get("resources"); + if (!resources.isEmpty()) { + namespaceOnlyApiTags.add(namespace.get("routeName") + "/" + resources.get(0).get("routeName")); + } + } + } + additionalProperties.put("rbNamespaces", rubyNamespaces); } private static String stripSlashes(String s) { return s == null ? "" : s.replaceAll("^/+", "").replaceAll("/+$", ""); } + private List> buildRubyNamespaces(OpenAPI openAPI) { + Map> namespaces = new TreeMap<>(); + Map> resourcesByNamespace = new TreeMap<>(); + if (openAPI != null && openAPI.getPaths() != null) { + for (Map.Entry pathEntry : openAPI.getPaths().entrySet()) { + PathItem pathItem = pathEntry.getValue(); + if (pathItem == null) continue; + for (Map.Entry operationEntry : pathItem.readOperationsMap().entrySet()) { + Operation operation = operationEntry.getValue(); + if (operation == null || (operation.getExtensions() != null + && Boolean.TRUE.equals(operation.getExtensions().get("x-internal")))) { + continue; + } + RubyApiRouting.Route route = RubyApiRouting.route( + pathEntry.getKey(), operationEntry.getKey().name(), operation.getOperationId(), + resourceSegments, apiBasePrefix); + Map namespace = namespaces.computeIfAbsent(route.namespace, k -> { + Map data = new HashMap<>(); + String base = underscore(sanitizeName(k.replace('-', '_'))); + data.put("routeName", k); + data.put("name", base); + data.put("accessor", safeAccessorName(base)); + data.put("className", toApiName(k)); + data.put("hasDirectOperations", false); + return data; + }); + if (route.resource == null) { + namespace.put("hasDirectOperations", true); + } else { + resourcesByNamespace.computeIfAbsent(route.namespace, k -> new TreeSet<>()).add(route.resource); + } + } + } + } + + List> result = new ArrayList<>(); + for (Map.Entry> entry : namespaces.entrySet()) { + Map namespace = entry.getValue(); + List> resources = new ArrayList<>(); + for (String resource : resourcesByNamespace.getOrDefault(entry.getKey(), Collections.emptySet())) { + Map resourceData = new HashMap<>(); + resourceData.put("routeName", resource); + resourceData.put("accessor", underscore(sanitizeName(resource.replace('-', '_')))); + resourceData.put("className", toApiName(entry.getKey() + "/" + resource)); + resources.add(resourceData); + } + namespace.put("resources", resources); + result.add(namespace); + } + return result; + } + + private Map findRubyNamespace(String routeName) { + if (routeName == null) { + return null; + } + for (Map namespace : rubyNamespaces) { + if (routeName.equals(namespace.get("routeName"))) { + return namespace; + } + } + return null; + } + + private String apiTagForFilename(String templateName, String tag) { + if (!"api.mustache".equals(templateName)) { + return tag; + } + int slash = tag.indexOf('/'); + // A namespace with only nested resource groups has no direct API file. Use the + // namespace filename for its resource group so api_operations.mustache can define the + // namespace class before the nested resource class and Zeitwerk can load it as a class. + if (slash > 0 && namespaceOnlyApiTags.contains(tag)) { + return tag.substring(0, slash); + } + return tag; + } + + @Override + public String apiFilename(String templateName, String tag) { + return super.apiFilename(templateName, apiTagForFilename(templateName, tag)); + } + + @Override + public String apiFilename(String templateName, String tag, String outputDir) { + return super.apiFilename(templateName, apiTagForFilename(templateName, tag), outputDir); + } + @Override public void addOperationToGroup(String tag, String resourcePath, Operation operation, CodegenOperation co, Map> operations) { @@ -742,6 +844,26 @@ public ModelsMap postProcessModels(ModelsMap objs) { public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) { objs = super.postProcessOperationsWithModels(objs, allModels); OperationMap ops = objs.getOperations(); + if (ops != null && !ops.getOperation().isEmpty()) { + CodegenOperation firstOperation = ops.getOperation().get(0); + String namespaceName = (String) firstOperation.vendorExtensions.get("x-rb-namespace"); + String resourceName = (String) firstOperation.vendorExtensions.get("x-rb-resource"); + Map namespace = findRubyNamespace(namespaceName); + if (namespace != null) { + boolean isNamespaceClass = ops.getClassname().equals(namespace.get("className")); + if (isNamespaceClass) { + ops.put("rbNamespaceHasDirectOperations", true); + ops.put("rbNamespaceResources", namespace.get("resources")); + ops.put("rbNamespaceAccessor", namespace.get("accessor")); + } else if (!Boolean.TRUE.equals(namespace.get("hasDirectOperations")) + && namespaceOnlyApiTags.contains(namespaceName + "/" + resourceName)) { + ops.put("rbNamespaceOnly", true); + ops.put("rbNamespaceClassName", namespace.get("className")); + ops.put("rbNamespaceResources", namespace.get("resources")); + ops.put("rbNamespaceAccessor", namespace.get("accessor")); + } + } + } for (CodegenOperation co : ops.getOperation()) { String rt; if (co.returnBaseType == null) { diff --git a/modules/openapi-generator/src/main/resources/ruby-nextgen/api_operations.mustache b/modules/openapi-generator/src/main/resources/ruby-nextgen/api_operations.mustache index dbbd89a88460..66e22b94b87d 100644 --- a/modules/openapi-generator/src/main/resources/ruby-nextgen/api_operations.mustache +++ b/modules/openapi-generator/src/main/resources/ruby-nextgen/api_operations.mustache @@ -1,3 +1,17 @@ +{{#rbNamespaceOnly}} +class {{rbNamespaceClassName}} + def initialize(connection) + @connection = connection + end +{{#rbNamespaceResources}} + + def {{accessor}} + @{{accessor}} ||= {{className}}.new(@connection) + end +{{/rbNamespaceResources}} +end + +{{/rbNamespaceOnly}} class {{classname}} def initialize(connection) @connection = connection @@ -34,4 +48,12 @@ class {{classname}} ) end {{/operation}} -end \ No newline at end of file +{{#rbNamespaceHasDirectOperations}} +{{#rbNamespaceResources}} + + def {{accessor}} + @{{accessor}} ||= {{className}}.new(@connection) + end +{{/rbNamespaceResources}} +{{/rbNamespaceHasDirectOperations}} +end diff --git a/modules/openapi-generator/src/main/resources/ruby-nextgen/api_test.mustache b/modules/openapi-generator/src/main/resources/ruby-nextgen/api_test.mustache index fb2b5f507aa2..ef85843a56d9 100644 --- a/modules/openapi-generator/src/main/resources/ruby-nextgen/api_test.mustache +++ b/modules/openapi-generator/src/main/resources/ruby-nextgen/api_test.mustache @@ -10,5 +10,21 @@ RSpec.describe {{moduleName}}::{{#apiNamespacePresent}}{{apiNamespace}}::{{/apiN api = described_class.new(client.connection) expect(api).to be_a(described_class) end +{{#rbNamespaceHasDirectOperations}} +{{#rbNamespaceResources}} + + it 'is reachable through the namespace client' do + expect(client.{{rbNamespaceAccessor}}.{{accessor}}).to be_a(described_class) + end +{{/rbNamespaceResources}} +{{/rbNamespaceHasDirectOperations}} +{{#rbNamespaceOnly}} +{{#rbNamespaceResources}} + + it 'is reachable through the namespace client' do + expect(client.{{rbNamespaceAccessor}}.{{accessor}}).to be_a(described_class) + end +{{/rbNamespaceResources}} +{{/rbNamespaceOnly}} end {{/operations}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rubynextgen/RubyNextgenClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rubynextgen/RubyNextgenClientCodegenTest.java index a3a5e95a1ab8..4047f8857dee 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rubynextgen/RubyNextgenClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rubynextgen/RubyNextgenClientCodegenTest.java @@ -398,4 +398,36 @@ public void testAcronymModelsGetZeitwerkInflections() throws Exception { org.openapitools.codegen.TestUtils.assertFileContains( target.resolve("lib/acme/api/dedicated_cloud/two_fa_whitelist.rb"), "TwoFAWhitelist"); } + + @Test + public void testNestedResourcesAreReachableFromClient() throws Exception { + java.nio.file.Path target = java.nio.file.Files.createTempDirectory("test"); + target.toFile().deleteOnExit(); + org.openapitools.codegen.ClientOptInput input = + new org.openapitools.codegen.config.CodegenConfigurator() + .setGeneratorName("ruby-nextgen") + .setInputSpec("src/test/resources/3_0/ruby-nextgen/nested-resources.yaml") + .setOutputDir(target.toString()) + .addAdditionalProperty("gemName", "petstore") + .addAdditionalProperty("moduleName", "Petstore") + .toClientOptInput(); + new org.openapitools.codegen.DefaultGenerator(false).opts(input).generate(); + + org.openapitools.codegen.TestUtils.assertFileContains( + target.resolve("lib/petstore/client.rb"), "def stables", "Petstore::Api::Stables.new"); + org.openapitools.codegen.TestUtils.assertFileContains( + target.resolve("lib/petstore/api/stables.rb"), + "def ponies", "@ponies ||= Stables::Ponies.new(@connection)"); + org.openapitools.codegen.TestUtils.assertFileExists( + target.resolve("lib/petstore/api/stables/ponies.rb")); + + java.nio.file.Path namespaceOnly = target.resolve("lib/petstore/api/only.rb"); + org.openapitools.codegen.TestUtils.assertFileContains( + namespaceOnly, "class Only", "def children", "@children ||= Only::Children.new(@connection)", + "def siblings", "@siblings ||= Only::Siblings.new(@connection)", "class Only::Children"); + org.openapitools.codegen.TestUtils.assertFileNotExists( + target.resolve("lib/petstore/api/only/children.rb")); + org.openapitools.codegen.TestUtils.assertFileExists( + target.resolve("lib/petstore/api/only/siblings.rb")); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/ruby-nextgen/nested-resources.yaml b/modules/openapi-generator/src/test/resources/3_0/ruby-nextgen/nested-resources.yaml new file mode 100644 index 000000000000..faba4c240b47 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/ruby-nextgen/nested-resources.yaml @@ -0,0 +1,74 @@ +openapi: 3.0.3 +info: + title: Nested resources + version: 1.0.0 +paths: + /stables: + get: + operationId: listStables + responses: + '200': + description: OK + /stables/{stable}: + get: + operationId: getStable + parameters: + - name: stable + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /stables/{stable}/ponies/{pony}: + get: + operationId: getPony + parameters: + - name: stable + in: path + required: true + schema: + type: string + - name: pony + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /only/{only}/children/{child}: + get: + operationId: getChild + parameters: + - name: only + in: path + required: true + schema: + type: string + - name: child + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /only/{only}/siblings/{sibling}: + get: + operationId: getSibling + parameters: + - name: only + in: path + required: true + schema: + type: string + - name: sibling + in: path + required: true + schema: + type: string + responses: + '200': + description: OK diff --git a/samples/client/petstore/ruby-nextgen/lib/petstore/api/store.rb b/samples/client/petstore/ruby-nextgen/lib/petstore/api/store.rb index c72e12aeaf6b..1d6751023c8f 100644 --- a/samples/client/petstore/ruby-nextgen/lib/petstore/api/store.rb +++ b/samples/client/petstore/ruby-nextgen/lib/petstore/api/store.rb @@ -15,6 +15,10 @@ def inventory auth: ['api_key'] ) end + + def order + @order ||= Store::Order.new(@connection) + end end end end diff --git a/samples/client/petstore/ruby-nextgen/spec/api/store_spec.rb b/samples/client/petstore/ruby-nextgen/spec/api/store_spec.rb index cf79fbb9e70f..3cac28f6a9b9 100644 --- a/samples/client/petstore/ruby-nextgen/spec/api/store_spec.rb +++ b/samples/client/petstore/ruby-nextgen/spec/api/store_spec.rb @@ -9,4 +9,8 @@ api = described_class.new(client.connection) expect(api).to be_a(described_class) end + + it 'is reachable through the namespace client' do + expect(client.store.order).to be_a(Petstore::Api::Store::Order) + end end From 9008f1f2e71137e3417c9cd92b8e91c1939b2d85 Mon Sep 17 00:00:00 2001 From: AxelRay <110029405+axelray-dev@users.noreply.github.com> Date: Wed, 23 Sep 2026 23:21:34 +0800 Subject: [PATCH 2/4] test(ruby-nextgen): assert nested resource class types --- .../src/main/resources/ruby-nextgen/api_test.mustache | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/ruby-nextgen/api_test.mustache b/modules/openapi-generator/src/main/resources/ruby-nextgen/api_test.mustache index ef85843a56d9..277d00d9373f 100644 --- a/modules/openapi-generator/src/main/resources/ruby-nextgen/api_test.mustache +++ b/modules/openapi-generator/src/main/resources/ruby-nextgen/api_test.mustache @@ -14,7 +14,7 @@ RSpec.describe {{moduleName}}::{{#apiNamespacePresent}}{{apiNamespace}}::{{/apiN {{#rbNamespaceResources}} it 'is reachable through the namespace client' do - expect(client.{{rbNamespaceAccessor}}.{{accessor}}).to be_a(described_class) + expect(client.{{rbNamespaceAccessor}}.{{accessor}}).to be_a({{moduleName}}::{{#apiNamespacePresent}}{{apiNamespace}}::{{/apiNamespacePresent}}{{className}}) end {{/rbNamespaceResources}} {{/rbNamespaceHasDirectOperations}} @@ -22,7 +22,7 @@ RSpec.describe {{moduleName}}::{{#apiNamespacePresent}}{{apiNamespace}}::{{/apiN {{#rbNamespaceResources}} it 'is reachable through the namespace client' do - expect(client.{{rbNamespaceAccessor}}.{{accessor}}).to be_a(described_class) + expect(client.{{rbNamespaceAccessor}}.{{accessor}}).to be_a({{moduleName}}::{{#apiNamespacePresent}}{{apiNamespace}}::{{/apiNamespacePresent}}{{className}}) end {{/rbNamespaceResources}} {{/rbNamespaceOnly}} From a445f4ac9f01d77ed7521dee05f88ac4c2fe088f Mon Sep 17 00:00:00 2001 From: AxelRay <110029405+axelray-dev@users.noreply.github.com> Date: Fri, 25 Sep 2026 05:47:19 +0800 Subject: [PATCH 3/4] fix(ruby-nextgen): respect generated API groups --- .../languages/RubyNextgenClientCodegen.java | 60 +++++++++++++------ 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyNextgenClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyNextgenClientCodegen.java index 663eed635ddd..2e57d61cdaf9 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyNextgenClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyNextgenClientCodegen.java @@ -535,15 +535,6 @@ public void preprocessOpenAPI(OpenAPI openAPI) { : RubyApiRouting.commonBasePrefix(paths); this.rubyNamespaces = buildRubyNamespaces(openAPI); this.namespaceOnlyApiTags.clear(); - for (Map namespace : rubyNamespaces) { - if (!Boolean.TRUE.equals(namespace.get("hasDirectOperations"))) { - @SuppressWarnings("unchecked") - List> resources = (List>) namespace.get("resources"); - if (!resources.isEmpty()) { - namespaceOnlyApiTags.add(namespace.get("routeName") + "/" + resources.get(0).get("routeName")); - } - } - } additionalProperties.put("rbNamespaces", rubyNamespaces); } @@ -554,6 +545,7 @@ private static String stripSlashes(String s) { private List> buildRubyNamespaces(OpenAPI openAPI) { Map> namespaces = new TreeMap<>(); Map> resourcesByNamespace = new TreeMap<>(); + Map> directOperationsByNamespace = new TreeMap<>(); if (openAPI != null && openAPI.getPaths() != null) { for (Map.Entry pathEntry : openAPI.getPaths().entrySet()) { PathItem pathItem = pathEntry.getValue(); @@ -579,6 +571,9 @@ private List> buildRubyNamespaces(OpenAPI openAPI) { }); if (route.resource == null) { namespace.put("hasDirectOperations", true); + directOperationsByNamespace + .computeIfAbsent(route.namespace, k -> new TreeSet<>()) + .add(toOperationId(route.action)); } else { resourcesByNamespace.computeIfAbsent(route.namespace, k -> new TreeSet<>()).add(route.resource); } @@ -590,10 +585,13 @@ private List> buildRubyNamespaces(OpenAPI openAPI) { for (Map.Entry> entry : namespaces.entrySet()) { Map namespace = entry.getValue(); List> resources = new ArrayList<>(); + Set reservedNames = directOperationsByNamespace + .getOrDefault(entry.getKey(), Collections.emptySet()); for (String resource : resourcesByNamespace.getOrDefault(entry.getKey(), Collections.emptySet())) { Map resourceData = new HashMap<>(); resourceData.put("routeName", resource); - resourceData.put("accessor", underscore(sanitizeName(resource.replace('-', '_')))); + String resourceAccessor = underscore(sanitizeName(resource.replace('-', '_'))); + resourceData.put("accessor", safeResourceAccessorName(resourceAccessor, reservedNames)); resourceData.put("className", toApiName(entry.getKey() + "/" + resource)); resources.add(resourceData); } @@ -715,11 +713,22 @@ private static String safeAccessorName(String name) { return RESERVED_ACCESSOR_NAMES.contains(name) ? name + "_api" : name; } + // Resource methods live on the namespace class beside its constructor and any direct + // operations. Rename a resource accessor deterministically when either would collide. + private static String safeResourceAccessorName(String name, Set reservedNames) { + String candidate = safeAccessorName(name); + while (reservedNames.contains(candidate)) { + candidate += "_api"; + } + return candidate; + } + @Override @SuppressWarnings("unchecked") public Map postProcessSupportingFileData(Map objs) { Map> nsMap = new TreeMap<>(); Map> resourcesByNs = new TreeMap<>(); + Map> directOperationsByNs = new TreeMap<>(); Map apiInfo = (Map) objs.get("apiInfo"); if (apiInfo != null) { List> apis = (List>) apiInfo.get("apis"); @@ -744,7 +753,11 @@ public Map postProcessSupportingFileData(Map obj m.put("className", toApiName(ns)); return m; }); - if (res != null) resourcesByNs.computeIfAbsent(ns, k -> new TreeSet<>()).add(res); + if (res != null) { + resourcesByNs.computeIfAbsent(ns, k -> new TreeSet<>()).add(res); + } else if (co.operationId != null) { + directOperationsByNs.computeIfAbsent(ns, k -> new TreeSet<>()).add(co.operationId); + } } } } @@ -753,9 +766,12 @@ public Map postProcessSupportingFileData(Map obj for (Map.Entry> e : nsMap.entrySet()) { Map m = e.getValue(); List> resources = new ArrayList<>(); + Set reservedNames = directOperationsByNs + .getOrDefault(e.getKey(), Collections.emptySet()); for (String res : resourcesByNs.getOrDefault(e.getKey(), Collections.emptySet())) { Map rm = new HashMap<>(); - rm.put("accessor", underscore(sanitizeName(res.replace('-', '_')))); + String resourceAccessor = underscore(sanitizeName(res.replace('-', '_'))); + rm.put("accessor", safeResourceAccessorName(resourceAccessor, reservedNames)); rm.put("className", toApiName(e.getKey() + "/" + res)); resources.add(rm); } @@ -855,12 +871,20 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List tag.startsWith(namespacePrefix)); + if (!namespaceFileClaimed) { + namespaceOnlyApiTags.add(resourceTag); + } + if (namespaceOnlyApiTags.contains(resourceTag)) { + ops.put("rbNamespaceOnly", true); + ops.put("rbNamespaceClassName", namespace.get("className")); + ops.put("rbNamespaceResources", namespace.get("resources")); + ops.put("rbNamespaceAccessor", namespace.get("accessor")); + } } } } From e52ac91ea6c4f43f068a7cfe70b63af83ad4f1e5 Mon Sep 17 00:00:00 2001 From: AxelRay <110029405+axelray-dev@users.noreply.github.com> Date: Mon, 28 Sep 2026 02:48:09 +0800 Subject: [PATCH 4/4] fix(ruby-nextgen): keep resource filenames independent --- .../codegen/languages/RubyNextgenClientCodegen.java | 4 +++- .../rubynextgen/RubyNextgenClientCodegenTest.java | 12 +++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyNextgenClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyNextgenClientCodegen.java index 2e57d61cdaf9..5b5e70b7b423 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyNextgenClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyNextgenClientCodegen.java @@ -591,6 +591,7 @@ private List> buildRubyNamespaces(OpenAPI openAPI) { Map resourceData = new HashMap<>(); resourceData.put("routeName", resource); String resourceAccessor = underscore(sanitizeName(resource.replace('-', '_'))); + resourceData.put("name", resourceAccessor); resourceData.put("accessor", safeResourceAccessorName(resourceAccessor, reservedNames)); resourceData.put("className", toApiName(entry.getKey() + "/" + resource)); resources.add(resourceData); @@ -771,6 +772,7 @@ public Map postProcessSupportingFileData(Map obj for (String res : resourcesByNs.getOrDefault(e.getKey(), Collections.emptySet())) { Map rm = new HashMap<>(); String resourceAccessor = underscore(sanitizeName(res.replace('-', '_'))); + rm.put("name", resourceAccessor); rm.put("accessor", safeResourceAccessorName(resourceAccessor, reservedNames)); rm.put("className", toApiName(e.getKey() + "/" + res)); resources.add(rm); @@ -798,7 +800,7 @@ public Map postProcessSupportingFileData(Map obj fileToConstant.putIfAbsent((String) ns.get("name"), lastConstantSegment((String) ns.get("className"))); List> resources = (List>) ns.get("resources"); for (Map res : resources) { - fileToConstant.putIfAbsent((String) res.get("accessor"), lastConstantSegment((String) res.get("className"))); + fileToConstant.putIfAbsent((String) res.get("name"), lastConstantSegment((String) res.get("className"))); } } List> inflections = new ArrayList<>(); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rubynextgen/RubyNextgenClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rubynextgen/RubyNextgenClientCodegenTest.java index 4047f8857dee..409a8feeff58 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rubynextgen/RubyNextgenClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rubynextgen/RubyNextgenClientCodegenTest.java @@ -390,13 +390,14 @@ public void testAcronymModelsGetZeitwerkInflections() throws Exception { // and the model file itself must define the acronym-cased constant org.openapitools.codegen.TestUtils.assertFileContains( target.resolve("lib/acme/models/http_config.rb"), "HTTPConfig"); - // Acronyms in API resource classes must be registered too: the file - // api/dedicated_cloud/two_fa_whitelist.rb defines DedicatedCloud::TwoFAWhitelist, - // which the default inflector (expecting TwoFaWhitelist) would fail to autoload. + // Acronyms in API resource classes must be registered too. In a namespace with + // nested resources only, the first resource group's file also defines the namespace + // class, so it is emitted as api/dedicated_cloud.rb rather than as a separate + // two_fa_whitelist.rb file. org.openapitools.codegen.TestUtils.assertFileContains( target.resolve("lib/acme.rb"), "\"two_fa_whitelist\" => \"TwoFAWhitelist\""); org.openapitools.codegen.TestUtils.assertFileContains( - target.resolve("lib/acme/api/dedicated_cloud/two_fa_whitelist.rb"), "TwoFAWhitelist"); + target.resolve("lib/acme/api/dedicated_cloud.rb"), "class DedicatedCloud::TwoFAWhitelist"); } @Test @@ -414,7 +415,8 @@ public void testNestedResourcesAreReachableFromClient() throws Exception { new org.openapitools.codegen.DefaultGenerator(false).opts(input).generate(); org.openapitools.codegen.TestUtils.assertFileContains( - target.resolve("lib/petstore/client.rb"), "def stables", "Petstore::Api::Stables.new"); + target.resolve("lib/petstore/client.rb"), "def stables", "Petstore::Api::Stables.new", + "def only", "Petstore::Api::Only.new"); org.openapitools.codegen.TestUtils.assertFileContains( target.resolve("lib/petstore/api/stables.rb"), "def ponies", "@ponies ||= Stables::Ponies.new(@connection)");