Skip to content
Open
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 @@ -51,7 +51,7 @@ class {{classname}} {
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'{{{path}}}'{{#pathParams}}.replaceAll('{' r'{{{baseName}}}' '}', {{#includeLibraryTemplate}}api/query_param{{/includeLibraryTemplate}}.toString()){{/pathParams}};
final _path = r'{{{path}}}'{{#pathParams}}.replaceAll('{' r'{{{baseName}}}' '}', {{#includeLibraryTemplate}}api/path_param{{/includeLibraryTemplate}},){{/pathParams}};
final _options = Options(
method: r'{{#lambda.uppercase}}{{httpMethod}}{{/lambda.uppercase}}',
{{#isResponseFile}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,12 @@ dev_dependencies:
build_runner: any
freezed: '^3.0.0'
json_serializable: '^6.9.3'
# unlike the json_serializable variant, this one has no copy_with_extension_gen
# whose source_gen constraint would hold source_helper back; source_helper 1.4.0
# added extension members that clash with json_serializable 6.x's internal
# copies and break the build_runner build script, so hold it below 1.4.0.
# 1.3.9+ requires Dart 3.9 while this variant advertises >=3.8.0, so keep the
# 1.3.8 lower bound resolvable for 3.8 users
source_helper: '>=1.3.8 <1.4.0'
{{/useFreezed}}
test: '^1.16.0'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Uri.encodeComponent({{#includeLibraryTemplate}}api/query_param{{/includeLibraryTemplate}}.toString())
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Uri.encodeComponent({{#includeLibraryTemplate}}api/query_param{{/includeLibraryTemplate}}.toString())
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Uri.encodeComponent({{#includeLibraryTemplate}}api/query_param{{/includeLibraryTemplate}}.toString())
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,16 @@ module {{moduleName}}
attr_reader :loader
end

{{#moduleNameNested}}
# Nested moduleName: for_gem can't target a nested namespace, so drive Zeitwerk
# explicitly. The parent module(s) above are pre-defined so `module {{moduleName}}`
# (here and in every generated file) reopens rather than raising NameError.
# for_gem derives the namespace from gemName, which need not match moduleName (and
# can't target a nested one), so drive Zeitwerk explicitly. Any parent module(s) above
# are pre-defined so `module {{moduleName}}` reopens rather than raising NameError.
@loader = Zeitwerk::Loader.new
@loader.push_dir("#{__dir__}/{{gemName}}", namespace: self)
# version.rb defines VERSION (not a `Version` class). for_gem's GemInflector special-cases
# this; a plain Loader doesn't, so ignore it for autoloading and require it directly to keep
# {{moduleName}}::VERSION available.
@loader.ignore("#{__dir__}/{{gemName}}/version.rb")
require_relative "{{gemName}}/version"
{{/moduleNameNested}}
{{^moduleNameNested}}
@loader = Zeitwerk::Loader.for_gem
{{/moduleNameNested}}
{{#zeitwerkInflections.0}}
# Model names keep their acronym casing (e.g. HTTPConfig), but Zeitwerk's default
# inflector would expect HttpConfig from the file name. Register the exceptions so
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,47 @@ public void testFreezedIgnoresUseOptional() throws IOException {
TestUtils.assertFileNotContains(output.toPath().resolve("lib/src/model/pet.dart"), "Optional<");
}

/**
* Path parameters must be URI-encoded as path components: the query_param helpers pass
* strings through unchanged, so a value containing '/' or spaces would otherwise break
* the request path. Every path substitution must therefore go through
* Uri.encodeComponent.
*/
@Test
public void testPathParamsAreUriEncoded() throws IOException {
for (Object[] libraryOption : serializationLibraries()) {
String serializationLibrary = (String) libraryOption[0];
File output = Files.createTempDirectory("test").toFile();
output.deleteOnExit();

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("dart-dio")
.setInputSpec("src/test/resources/3_0/petstore.yaml")
.setOutputDir(output.getAbsolutePath().replace("\\", "/"))
.addAdditionalProperty(CodegenConstants.SERIALIZATION_LIBRARY, serializationLibrary);

ClientOptInput opts = configurator.toClientOptInput();
Generator generator = new DefaultGenerator().opts(opts);
List<File> files = generator.generate();
files.forEach(File::deleteOnExit);

// check every generated api class, not just one
Path apiDir = output.toPath().resolve("lib/src/api");
try (var apiFiles = Files.list(apiDir)) {
for (Path apiFile : apiFiles.filter(p -> p.getFileName().toString().endsWith("_api.dart"))
.collect(java.util.stream.Collectors.toList())) {
String content = Files.readString(apiFile, StandardCharsets.UTF_8);
int substitutions = content.split("\\.replaceAll\\(", -1).length - 1;
Assert.assertTrue(substitutions > 0,
"expected path substitutions in " + apiFile + ": " + content);
int encodings = content.split("Uri\\.encodeComponent\\(", -1).length - 1;
Assert.assertEquals(encodings, substitutions,
"every path substitution in " + apiFile + " must be URI-encoded: " + content);
}
}
}
}

private Path generateFormParamsClient(String serializationLibrary) throws IOException {
File output = Files.createTempDirectory("test").toFile();
output.deleteOnExit();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,31 @@ public void testAcronymModelsGetZeitwerkInflections() throws Exception {
"@two_fa_whitelist ||= DedicatedCloud::TwoFAWhitelist.new(@connection)");
}

@Test
public void testZeitwerkLoaderWhenModuleNameDiffersFromGemName() 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/acronym.yaml")
.setOutputDir(target.toString())
// camelized gemName would be ThingsApi, but the namespace is Things:
// Zeitwerk::Loader.for_gem would target ThingsApi and autoload nothing
.addAdditionalProperty("gemName", "things_api")
.addAdditionalProperty("moduleName", "Things")
.toClientOptInput();
new org.openapitools.codegen.DefaultGenerator(false).opts(input).generate();

// the explicit loader targets the moduleName namespace regardless of gemName
org.openapitools.codegen.TestUtils.assertFileContains(
target.resolve("lib/things_api.rb"),
"Zeitwerk::Loader.new",
"@loader.push_dir(\"#{__dir__}/things_api\", namespace: self)");
org.openapitools.codegen.TestUtils.assertFileNotContains(
target.resolve("lib/things_api.rb"), "Zeitwerk::Loader.for_gem");
}

@Test
public void testNestedResourcesAreReachableFromClient() throws Exception {
java.nio.file.Path target = java.nio.file.Files.createTempDirectory("test");
Expand Down
11 changes: 10 additions & 1 deletion samples/client/others/ruby-nextgen-qdrant/lib/qdrant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@ class << self
attr_reader :loader
end

@loader = Zeitwerk::Loader.for_gem
# for_gem derives the namespace from gemName, which need not match moduleName (and
# can't target a nested one), so drive Zeitwerk explicitly. Any parent module(s) above
# are pre-defined so `module Qdrant` reopens rather than raising NameError.
@loader = Zeitwerk::Loader.new
@loader.push_dir("#{__dir__}/qdrant", namespace: self)
# version.rb defines VERSION (not a `Version` class). for_gem's GemInflector special-cases
# this; a plain Loader doesn't, so ignore it for autoloading and require it directly to keep
# Qdrant::VERSION available.
@loader.ignore("#{__dir__}/qdrant/version.rb")
require_relative "qdrant/version"
# The `api/` directory on disk is fixed regardless of the `apiNamespace` CliOption,
# but api.mustache only nests classes under an `Api` module when the wrapper is
# enabled (true). When it's disabled, tell Zeitwerk to collapse
Expand Down
11 changes: 10 additions & 1 deletion samples/client/petstore/ruby-nextgen/lib/petstore.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@ class << self
attr_reader :loader
end

@loader = Zeitwerk::Loader.for_gem
# for_gem derives the namespace from gemName, which need not match moduleName (and
# can't target a nested one), so drive Zeitwerk explicitly. Any parent module(s) above
# are pre-defined so `module Petstore` reopens rather than raising NameError.
@loader = Zeitwerk::Loader.new
@loader.push_dir("#{__dir__}/petstore", namespace: self)
# version.rb defines VERSION (not a `Version` class). for_gem's GemInflector special-cases
# this; a plain Loader doesn't, so ignore it for autoloading and require it directly to keep
# Petstore::VERSION available.
@loader.ignore("#{__dir__}/petstore/version.rb")
require_relative "petstore/version"
# The `api/` directory on disk is fixed regardless of the `apiNamespace` CliOption,
# but api.mustache only nests classes under an `Api` module when the wrapper is
# enabled (true). When it's disabled, tell Zeitwerk to collapse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ class PetApi {
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', encodeQueryParameter(_serializers, petId, const FullType(int)).toString());
final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, petId, const FullType(int)).toString()),);
final _options = Options(
method: r'DELETE',
headers: <String, dynamic>{
Expand Down Expand Up @@ -370,7 +370,7 @@ class PetApi {
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', encodeQueryParameter(_serializers, petId, const FullType(int)).toString());
final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, petId, const FullType(int)).toString()),);
final _options = Options(
method: r'GET',
headers: <String, dynamic>{
Expand Down Expand Up @@ -558,7 +558,7 @@ class PetApi {
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', encodeQueryParameter(_serializers, petId, const FullType(int)).toString());
final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, petId, const FullType(int)).toString()),);
final _options = Options(
method: r'POST',
headers: <String, dynamic>{
Expand Down Expand Up @@ -636,7 +636,7 @@ class PetApi {
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/pet/{petId}/uploadImage'.replaceAll('{' r'petId' '}', encodeQueryParameter(_serializers, petId, const FullType(int)).toString());
final _path = r'/pet/{petId}/uploadImage'.replaceAll('{' r'petId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, petId, const FullType(int)).toString()),);
final _options = Options(
method: r'POST',
headers: <String, dynamic>{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class StoreApi {
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/store/order/{orderId}'.replaceAll('{' r'orderId' '}', encodeQueryParameter(_serializers, orderId, const FullType(String)).toString());
final _path = r'/store/order/{orderId}'.replaceAll('{' r'orderId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, orderId, const FullType(String)).toString()),);
final _options = Options(
method: r'DELETE',
headers: <String, dynamic>{
Expand Down Expand Up @@ -170,7 +170,7 @@ class StoreApi {
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/store/order/{orderId}'.replaceAll('{' r'orderId' '}', encodeQueryParameter(_serializers, orderId, const FullType(int)).toString());
final _path = r'/store/order/{orderId}'.replaceAll('{' r'orderId' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, orderId, const FullType(int)).toString()),);
final _options = Options(
method: r'GET',
headers: <String, dynamic>{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ class UserApi {
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/user/{username}'.replaceAll('{' r'username' '}', encodeQueryParameter(_serializers, username, const FullType(String)).toString());
final _path = r'/user/{username}'.replaceAll('{' r'username' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, username, const FullType(String)).toString()),);
final _options = Options(
method: r'DELETE',
headers: <String, dynamic>{
Expand Down Expand Up @@ -320,7 +320,7 @@ class UserApi {
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/user/{username}'.replaceAll('{' r'username' '}', encodeQueryParameter(_serializers, username, const FullType(String)).toString());
final _path = r'/user/{username}'.replaceAll('{' r'username' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, username, const FullType(String)).toString()),);
final _options = Options(
method: r'GET',
headers: <String, dynamic>{
Expand Down Expand Up @@ -529,7 +529,7 @@ class UserApi {
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/user/{username}'.replaceAll('{' r'username' '}', encodeQueryParameter(_serializers, username, const FullType(String)).toString());
final _path = r'/user/{username}'.replaceAll('{' r'username' '}', Uri.encodeComponent(encodeQueryParameter(_serializers, username, const FullType(String)).toString()),);
final _options = Options(
method: r'PUT',
headers: <String, dynamic>{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class PetApi {
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', petId.toString());
final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', Uri.encodeComponent(petId.toString()),);
final _options = Options(
method: r'DELETE',
headers: <String, dynamic>{
Expand Down Expand Up @@ -336,7 +336,7 @@ _responseData = rawData == null ? null : deserialize<Set<Pet>, Pet>(rawData, 'Se
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', petId.toString());
final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', Uri.encodeComponent(petId.toString()),);
final _options = Options(
method: r'GET',
headers: <String, dynamic>{
Expand Down Expand Up @@ -494,7 +494,7 @@ _responseData = rawData == null ? null : deserialize<Pet, Pet>(rawData, 'Pet', g
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', petId.toString());
final _path = r'/pet/{petId}'.replaceAll('{' r'petId' '}', Uri.encodeComponent(petId.toString()),);
final _options = Options(
method: r'POST',
headers: <String, dynamic>{
Expand Down Expand Up @@ -572,7 +572,7 @@ _responseData = rawData == null ? null : deserialize<Pet, Pet>(rawData, 'Pet', g
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/pet/{petId}/uploadImage'.replaceAll('{' r'petId' '}', petId.toString());
final _path = r'/pet/{petId}/uploadImage'.replaceAll('{' r'petId' '}', Uri.encodeComponent(petId.toString()),);
final _options = Options(
method: r'POST',
headers: <String, dynamic>{
Expand Down Expand Up @@ -675,7 +675,7 @@ _responseData = rawData == null ? null : deserialize<ApiResponse, ApiResponse>(r
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/fake/{petId}/uploadImageWithRequiredFile'.replaceAll('{' r'petId' '}', petId.toString());
final _path = r'/fake/{petId}/uploadImageWithRequiredFile'.replaceAll('{' r'petId' '}', Uri.encodeComponent(petId.toString()),);
final _options = Options(
method: r'POST',
headers: <String, dynamic>{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class StoreApi {
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/store/order/{order_id}'.replaceAll('{' r'order_id' '}', orderId.toString());
final _path = r'/store/order/{order_id}'.replaceAll('{' r'order_id' '}', Uri.encodeComponent(orderId.toString()),);
final _options = Options(
method: r'DELETE',
headers: <String, dynamic>{
Expand Down Expand Up @@ -164,7 +164,7 @@ _responseData = rawData == null ? null : deserialize<Map<String, int>, int>(rawD
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/store/order/{order_id}'.replaceAll('{' r'order_id' '}', orderId.toString());
final _path = r'/store/order/{order_id}'.replaceAll('{' r'order_id' '}', Uri.encodeComponent(orderId.toString()),);
final _options = Options(
method: r'GET',
headers: <String, dynamic>{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ class UserApi {
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/user/{username}'.replaceAll('{' r'username' '}', username.toString());
final _path = r'/user/{username}'.replaceAll('{' r'username' '}', Uri.encodeComponent(username.toString()),);
final _options = Options(
method: r'DELETE',
headers: <String, dynamic>{
Expand Down Expand Up @@ -297,7 +297,7 @@ class UserApi {
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/user/{username}'.replaceAll('{' r'username' '}', username.toString());
final _path = r'/user/{username}'.replaceAll('{' r'username' '}', Uri.encodeComponent(username.toString()),);
final _options = Options(
method: r'GET',
headers: <String, dynamic>{
Expand Down Expand Up @@ -496,7 +496,7 @@ _responseData = rawData == null ? null : deserialize<String, String>(rawData, 'S
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
final _path = r'/user/{username}'.replaceAll('{' r'username' '}', username.toString());
final _path = r'/user/{username}'.replaceAll('{' r'username' '}', Uri.encodeComponent(username.toString()),);
final _options = Options(
method: r'PUT',
headers: <String, dynamic>{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,11 @@ dev_dependencies:
build_runner: any
freezed: '^3.0.0'
json_serializable: '^6.9.3'
# unlike the json_serializable variant, this one has no copy_with_extension_gen
# whose source_gen constraint would hold source_helper back; source_helper 1.4.0
# added extension members that clash with json_serializable 6.x's internal
# copies and break the build_runner build script, so hold it below 1.4.0.
# 1.3.9+ requires Dart 3.9 while this variant advertises >=3.8.0, so keep the
# 1.3.8 lower bound resolvable for 3.8 users
source_helper: '>=1.3.8 <1.4.0'
test: '^1.16.0'
Loading
Loading