Skip to content

Commit c08164d

Browse files
committed
fix: add support for remote specs without extensions. Closes #176
1 parent c715942 commit c08164d

File tree

4 files changed

+111
-14
lines changed

4 files changed

+111
-14
lines changed

openapi-generator/lib/src/gen_on_spec_changes.dart

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,45 @@ final _supportedRegexes = [jsonRegex, yamlRegex];
2727
/// It also throws an error when the specification doesn't exist on disk.
2828
///
2929
/// WARNING: THIS DOESN'T VALIDATE THE SPECIFICATION CONTENT
30-
FutureOr<Map<String, dynamic>> loadSpec(
31-
{required InputSpec specConfig, bool isCached = false}) async {
30+
FutureOr<Map<String, dynamic>> loadSpec({required InputSpec specConfig,
31+
bool isCached = false,
32+
http.Client? client}) async {
33+
client ??= http.Client();
3234
print('loadSpec - ' + specConfig.path);
3335
// If the spec file doesn't match any of the currently supported spec formats
3436
// reject the request.
3537
if (!_supportedRegexes
3638
.any((fileEnding) => fileEnding.hasMatch(specConfig.path))) {
37-
return Future.error(
38-
OutputMessage(
39+
if (specConfig is RemoteSpec) {
40+
final resp = await client.head(specConfig.url);
41+
if (resp.statusCode != 200) {
42+
return Future.error(OutputMessage(
43+
message:
44+
'Failed to fetch headers for remote spec. Status code: ${resp.statusCode}',
45+
level: Level.SEVERE,
46+
stackTrace: StackTrace.current,
47+
));
48+
}
49+
50+
var contentType = resp.headers['content-type'] ?? "unknown";
51+
if (!(contentType.contains('application/json') ||
52+
contentType.contains('yaml') ||
53+
contentType.contains('text/yaml') ||
54+
contentType.contains('application/x-yaml'))) {
55+
return Future.error(OutputMessage(
56+
message:
57+
'Invalid remote spec file format. Expected JSON or YAML but got $contentType.',
58+
level: Level.SEVERE,
59+
stackTrace: StackTrace.current,
60+
));
61+
}
62+
} else {
63+
return Future.error(OutputMessage(
3964
message: 'Invalid spec file format.',
4065
level: Level.SEVERE,
4166
stackTrace: StackTrace.current,
42-
),
43-
);
67+
));
68+
}
4469
}
4570

4671
if (!(specConfig is RemoteSpec)) {
@@ -78,7 +103,7 @@ FutureOr<Map<String, dynamic>> loadSpec(
78103
headers = specConfig.headerDelegate.header();
79104
}
80105

81-
final resp = await http.get(specConfig.url, headers: headers);
106+
final resp = await client.get(specConfig.url, headers: headers);
82107
if (resp.statusCode == 200) {
83108
if (yamlRegex.hasMatch(specConfig.path)) {
84109
return convertYamlMapToDartMap(yamlMap: loadYaml(resp.body));

openapi-generator/test/builder_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class TestClassConfig extends OpenapiGeneratorConfig {}
118118
.first;
119119
final args = GeneratorArguments(annotations: annotations);
120120
expect(
121-
(await args.jarArgs).join(' '),
121+
args.jarArgs.join(' '),
122122
contains('''
123123
generate -o=api/petstore_api -i=../openapi-spec.yaml -g=dart-dio --type-mappings=Pet=ExamplePet --additional-properties=allowUnicodeIdentifiers=false,ensureUniqueParams=true,useEnumExtension=true,enumUnknownDefaultCase=false,prependFormOrBodyParameters=false,pubAuthor=Johnny dep...,pubName=petstore_api,legacyDiscriminatorBehavior=true,sortModelPropertiesByRequiredFlag=true,sortParamsByRequiredFlag=true,wrapper=none
124124
'''

openapi-generator/test/gen_on_spec_changes_test.dart

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:convert';
22
import 'dart:io';
33

44
import 'package:http/http.dart' as http;
5+
import 'package:http/testing.dart';
56
import 'package:logging/logging.dart';
67
import 'package:openapi_generator/src/gen_on_spec_changes.dart';
78
import 'package:openapi_generator/src/models/output_message.dart';
@@ -32,6 +33,40 @@ void main() {
3233
expect((e as OutputMessage).message, 'Invalid spec file format.');
3334
}
3435
});
36+
37+
test('fails when local file has unsupported extension', () async {
38+
final specFuture =
39+
loadSpec(specConfig: InputSpec(path: './invalid.spec'));
40+
await expectLater(
41+
specFuture,
42+
throwsA(isA<OutputMessage>().having((e) => e.message, 'message',
43+
contains('Invalid spec file format'))));
44+
});
45+
test('supports json remote spec without extension', () async {
46+
final url =
47+
Uri.parse('https://example.com/api'); // Mock remote spec URL
48+
final mockResponse = http.Response('{}', 200,
49+
headers: {'content-type': 'application/json'});
50+
51+
// Mock HTTP HEAD request
52+
final client = MockClient((request) async => mockResponse);
53+
final specFuture = loadSpec(
54+
specConfig: RemoteSpec(path: url.toString()), client: client);
55+
await expectLater(specFuture, completion(isNot(throwsA(anything))));
56+
});
57+
test('supports remote yaml spec without extension', () async {
58+
final url =
59+
Uri.parse('https://example.com/api-yaml'); // Mock remote spec URL
60+
final mockResponse = http.Response('key: value', 200,
61+
headers: {'content-type': 'application/x-yaml'});
62+
63+
// Mock HTTP HEAD request
64+
final client = MockClient((request) async => mockResponse);
65+
final specFuture = loadSpec(
66+
specConfig: RemoteSpec(path: url.toString()), client: client);
67+
68+
await expectLater(specFuture, completion(isNot(throwsA(anything))));
69+
});
3570
test('throws an error for missing config file', () async {
3671
try {
3772
await loadSpec(
@@ -114,6 +149,43 @@ void main() {
114149
'Unable to request remote spec. Ensure it is public or use a local copy instead.');
115150
}
116151
});
152+
153+
test('fails when HEAD request returns non-200 status', () async {
154+
final url = Uri.parse('https://example.com/api-invalid');
155+
final mockResponse = http.Response('', 404);
156+
final client = MockClient((request) async => mockResponse);
157+
final specFuture = loadSpec(
158+
specConfig: RemoteSpec(path: url.toString()), client: client);
159+
await expectLater(
160+
specFuture,
161+
throwsA(isA<OutputMessage>().having((e) => e.message, 'message',
162+
contains('Failed to fetch headers'))));
163+
});
164+
165+
test('fails when Content-Type is missing', () async {
166+
final url = Uri.parse('https://example.com/api-no-content-type');
167+
final mockResponse = http.Response('', 200, headers: {});
168+
final client = MockClient((request) async => mockResponse);
169+
final specFuture = loadSpec(
170+
specConfig: RemoteSpec(path: url.toString()), client: client);
171+
await expectLater(
172+
specFuture,
173+
throwsA(isA<OutputMessage>().having((e) => e.message, 'message',
174+
contains('Invalid remote spec file format'))));
175+
});
176+
177+
test('fails when Content-Type is not JSON or YAML', () async {
178+
final url = Uri.parse('https://example.com/api-invalid-type');
179+
final mockResponse =
180+
http.Response('', 200, headers: {'content-type': 'text/plain'});
181+
final client = MockClient((request) async => mockResponse);
182+
final specFuture = loadSpec(
183+
specConfig: RemoteSpec(path: url.toString()), client: client);
184+
await expectLater(
185+
specFuture,
186+
throwsA(isA<OutputMessage>().having((e) => e.message, 'message',
187+
contains('Invalid remote spec file format'))));
188+
});
117189
});
118190
});
119191
group('verifies dirty status', () {

openapi-generator/test/generator_arguments_test.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ void main() {
6060
Directory.current.path + '${Platform.pathSeparator}openapi.json');
6161
f.createSync();
6262
f.writeAsStringSync('');
63-
expect(await args.jarArgs, [
63+
expect(args.jarArgs, [
6464
'generate',
6565
'-i=${args.inputFileOrFetch}',
6666
'-g=${args.generatorName}',
@@ -172,7 +172,7 @@ void main() {
172172
expect(args.forceAlwaysRun, isTrue);
173173
expect(args.additionalProperties?.useEnumExtension, isTrue);
174174
expect(args.additionalProperties?.pubAuthor, 'test author');
175-
expect(await args.jarArgs, [
175+
expect(args.jarArgs, [
176176
'generate',
177177
'-o=${args.outputDirectory}',
178178
'-i=${args.inputFileOrFetch}',
@@ -230,7 +230,7 @@ void main() {
230230
(args.additionalProperties as DioProperties).serializationLibrary,
231231
DioSerializationLibrary.jsonSerializable);
232232

233-
expect(await args.jarArgs, [
233+
expect(args.jarArgs, [
234234
'generate',
235235
'-o=${args.outputDirectory}',
236236
'-i=${args.inputFileOrFetch}',
@@ -287,11 +287,11 @@ void main() {
287287
(args.additionalProperties as DioAltProperties)
288288
.pubspecDevDependencies,
289289
'pedantic: 1.0.0');
290-
expect((await args.jarArgs).last, contains('path: 1.0.0'));
291-
expect((await args.jarArgs).last, contains('pedantic: 1.0.0'));
290+
expect((args.jarArgs).last, contains('path: 1.0.0'));
291+
expect((args.jarArgs).last, contains('pedantic: 1.0.0'));
292292
expect(args.additionalProperties.runtimeType, DioAltProperties);
293293

294-
expect(await args.jarArgs, [
294+
expect(args.jarArgs, [
295295
'generate',
296296
'-o=${args.outputDirectory}',
297297
'-i=${args.inputFileOrFetch}',

0 commit comments

Comments
 (0)