Skip to content

Commit 4108a1f

Browse files
committed
support disabling copyWith
1 parent b543359 commit 4108a1f

File tree

7 files changed

+209
-3
lines changed

7 files changed

+209
-3
lines changed

README.adoc

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ openApiGenerate {
9696

9797
Please refer to the article https://medium.com/@irinasouthwell_220/accelerate-flutter-development-with-openapi-and-dart-code-generation-1f16f8329a6a[on Medium].
9898

99+
Please not the versions of all the libraries have moved on since then.
100+
99101
==== Additional Properties
100102

101103
Additional properties allow you to customise how code is generated and we honour 2 of them above the normal ones.
@@ -108,6 +110,7 @@ of Dart to 2.12 and generate null safe code. Using the nullSafe-array-default, i
108110
as being `required` in your OpenAPI "required" but making them always generate a default value of `[]`. This ends up
109111
being considerably easier to use.
110112
- `listAnyOf=false` - this will turn _off_ AnyOf support. This would be a bit weird, but you can do it if you want.
113+
- `disableCopyWith` - if this is specified, then the copyWith functionality will be disabled. On complex OpenAPI definitions, the combinations of null safety and nested classes can cause incomplete or invalid code to be generated. We recommend disabling the generation of the copy-with code for that purpose. It is simply a convenience for coding and is not required as part of the API.
111114

112115
the normal ones include:
113116

@@ -247,6 +250,126 @@ behind as you generate.
247250
</plugin>
248251
----
249252

253+
=== I need to do something special with the Dio layer!
254+
255+
The DioClientDelegate we provide can be fully overridden - in Dart all classes are also interfaces so if you wish
256+
to do special things in the underlying "guts" of the Dio library you can easily do so. Caching is an example, one
257+
of our users has an https://gist.github.com/Maczuga/255e822a09f8d3dd8284096e5cda3003[example]:
258+
259+
Ensure you include 304 as a valid return type for cached APIs.
260+
261+
[source,dart]
262+
----
263+
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
264+
import 'package:openapi_dart_common/openapi.dart';
265+
import 'package:xxxx/api_delegate.dart'; // The file below
266+
267+
final _cacheOptions = CacheOptions(
268+
store: MemCacheStore(),
269+
hitCacheOnErrorExcept: [401, 403],
270+
maxStale: const Duration(days: 7),
271+
);
272+
273+
class API extends ApiClient {
274+
API._internal() : super(basePath: "https://xxxxxx/api") {
275+
apiClientDelegate = CustomDioClientDelegate();
276+
final dioDelegate = apiClientDelegate as CustomDioClientDelegate;
277+
dioDelegate.client.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
278+
}
279+
}
280+
----
281+
282+
[source,dart]
283+
----
284+
import 'dart:convert';
285+
286+
import 'package:dio/dio.dart';
287+
import 'package:openapi_dart_common/openapi.dart';
288+
289+
class CustomDioClientDelegate implements DioClientDelegate {
290+
@override
291+
final Dio client;
292+
293+
CustomDioClientDelegate([Dio? client]) : client = client ?? Dio();
294+
295+
@override
296+
Future<ApiResponse> invokeAPI(
297+
String basePath, String path, Iterable<QueryParam> queryParams, Object? body, Options options,
298+
{bool passErrorsAsApiResponses = false}) async {
299+
final String url = basePath + path;
300+
301+
// fill in query parameters
302+
final Map<String, String> qParams = _convertQueryParams(queryParams);
303+
304+
options.responseType = ResponseType.plain;
305+
options.receiveDataWhenStatusError = true;
306+
307+
// Dio can't cope with this in both places, it just adds them together in a stupid way
308+
if (options.headers != null && options.headers!['Content-Type'] != null) {
309+
options.contentType = options.headers!['Content-Type']?.toString();
310+
options.headers!.remove('Content-Type');
311+
}
312+
313+
try {
314+
Response<String> response;
315+
316+
if (['GET', 'HEAD', 'DELETE'].contains(options.method)) {
317+
response = await client.request<String>(url, options: options, queryParameters: qParams);
318+
} else {
319+
response = await client.request<String>(url, options: options, data: body, queryParameters: qParams);
320+
}
321+
322+
final stream = _jsonToStream(response.data);
323+
return ApiResponse(response.statusCode ?? 500, _convertHeaders(response.headers), stream);
324+
} catch (ex, stack) {
325+
if (ex is! DioError) rethrow;
326+
327+
if (passErrorsAsApiResponses) {
328+
if (ex.response == null) {
329+
return ApiResponse(500, {}, null)
330+
..innerException = ex
331+
..stackTrace = stack;
332+
}
333+
334+
// if (e.response.data)
335+
if (ex.response!.data is String?) {
336+
final response = ex.response!;
337+
final json = response.data as String;
338+
339+
return ApiResponse(response.statusCode ?? 500, _convertHeaders(response.headers), _jsonToStream(json));
340+
} else {
341+
print(
342+
"ex is not 'String?' ${ex.response.runtimeType.toString()} ${ex.response!.data?.runtimeType.toString() ?? ''}");
343+
}
344+
}
345+
346+
if (ex.response == null) {
347+
throw ApiException.withInner(500, 'Connection error', ex, stack);
348+
} else {
349+
throw ApiException.withInner(ex.response?.statusCode ?? 500, ex.response?.data as String?, ex, stack);
350+
}
351+
}
352+
}
353+
354+
Map<String, String> _convertQueryParams(Iterable<QueryParam> queryParams) {
355+
final Map<String, String> qp = {};
356+
for (final q in queryParams) {
357+
qp[q.name] = q.value;
358+
}
359+
return qp;
360+
}
361+
362+
Map<String, List<String>> _convertHeaders(Headers headers) {
363+
final Map<String, List<String>> res = {};
364+
headers.forEach((k, v) => res[k] = v);
365+
return res;
366+
}
367+
368+
Stream<List<int>> _jsonToStream(String? json) {
369+
return Stream<List<int>>.value(utf8.encode(json ?? ""));
370+
}
371+
}
372+
----
250373
=== Testing
251374

252375
If you are trying to make changes to the repository, I recommend adding a new test to "SampleRunner"
@@ -273,6 +396,7 @@ The source for the tests is located in src/k8s** folders. The generated test out
273396

274397
==== Changelog
275398

399+
- 5.13 - ability to disable the copyWith generation (see above)
276400
- 5.12 - contributed fixes for inherited types (via https://github.com/roald-di)
277401
- 5.11 - fix date/datetime strings in queries to not be encoded. Updated to use 5.2.1 of OpenAPI. Fixed a bunch
278402
of templating issues that arose because of it.
@@ -352,3 +476,13 @@ won't change method calls when you are only passing one type.
352476
- We will wrap exceptions that have generated models
353477
- We intend to be generating server side code for supporting Dart server side applications.
354478
- We are considering memoization
479+
480+
481+
==== Known Issues
482+
483+
- When you have a nullable Map type with a default value (e.g. {}), e.g. Map<String, Filter>? then the copyWith does
484+
not function correctly because `{} as Map<String, Filter>?` is not correct and `Map<String, Filter>?.from({})` is not
485+
valid syntax. We will need to use the Dart 2.16 functionality to create a new type for this map and then we can resolve
486+
the issue.
487+
- nullable: true is being removed from OpenAPI is not part of the standard form 3.1 onwards. You *can* specify a field
488+
as being required and being nullable: true, but this generator expects that required fields are not nullable.

src/main/java/cd/connect/openapi/DartV3ApiGenerator.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,8 @@ public void postProcess() {
668668
if (flutterDir != null && isEnablePostProcessFile()) {
669669
// String dartPostProcessFixFile = String.format("%s/bin/cache/dart-sdk/bin/dart fix --apply %s", flutterDir,
670670
// getOutputDir());
671-
String dartPostProcessFile = String.format("%s/bin/cache/dart-sdk/bin/dartfmt -w %s", flutterDir, getOutputDir());
671+
String dartPostProcessFile = String.format("%s/bin/cache/dart-sdk/bin/dart format -o write %s", flutterDir,
672+
getOutputDir());
672673

673674
try {
674675
// log.info("auto-fixing generated issues");
@@ -680,7 +681,7 @@ public void postProcess() {
680681
outputStreamToConsole(fmt);
681682
fmt.waitFor();
682683
} catch (Exception e) {
683-
log.error("Unable to run dart fix command");
684+
log.error("Unable to run dart fix command", e);
684685
}
685686
}
686687
}

src/main/resources/dart2-v3template/_any_of_class.mustache

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,14 @@ class {{classname}} {
7878
return hashCode;
7979
}
8080

81+
{{^disableCopyWith}}
8182
{{classname}} copyWith() {
8283
final copy = {{classname}}();
8384
copy._value = _value.copyWith();
8485
copy._discriminator = _discriminator;
8586
return copy;
8687
}
88+
{{/disableCopyWith}}
8789

8890
static List<{{classname}}> listFromJson(List<dynamic>{{#nullSafe}}?{{/nullSafe}} json) {
8991
return json == null ? <{{classname}}>[] : json.map((value) => {{classname}}.fromJson(value)).toList();

src/main/resources/dart2-v3template/class.mustache

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ class {{classname}}{{#parent}} extends {{parent}}{{/parent}} {
220220
return hashCode;
221221
}
222222

223+
{{^disableCopyWith}}
223224
{{classname}} copyWith({{#hasVars}}{
224225
{{#vars}}
225226
{{{dataType}}}{{#required}}{{#nullSafe}}?{{/nullSafe}}{{/required}}{{^required}}{{#vendorExtensions.x-ns-default-val}}?{{/vendorExtensions.x-ns-default-val}}{{/required}} {{{name}}},
@@ -252,6 +253,7 @@ class {{classname}}{{#parent}} extends {{parent}}{{/parent}} {
252253
return {{{classname}}}(
253254
{{#vars}}{{{name}}}: _copy_{{{name}}},{{/vars}});
254255
}
256+
{{/disableCopyWith}}
255257
}
256258

257259
{{#vars}}

src/main/resources/dart2-v3template/enum.mustache

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ extension {{classname}}Extension on {{classname}} {
2525
static List<{{classname}}> listFromJson(List<dynamic>{{#nullSafe}}?{{/nullSafe}} json) =>
2626
json == null ? <{{classname}}>[] : json.map((value) => fromJson(value)).toList(){{#nullSafe}}.fromNull(){{/nullSafe}};
2727

28+
{{^disableCopyWith}}
2829
static {{classname}} copyWith({{classname}} instance) => instance;
30+
{{/disableCopyWith}}
2931

3032
static Map<String, {{classname}}> mapFromJson(Map<String, dynamic>{{#nullSafe}}?{{/nullSafe}} json) {
3133
final map = <String, {{classname}}>{};

src/test/java/cd/connect/openapi/SampleRunner.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
public class SampleRunner {
1212
@Test
1313
public void runGenerator() {
14-
String location = getClass().getResource("/test.yaml").getFile();
14+
String location = getClass().getResource("/copy-with.yaml").getFile();
1515
// String location = getClass().getResource("/k8s_null_test.yml").getFile();
1616
// OpenAPIGenerator.main(Arrays.asList("help", "generate").toArray(new String[0]));
1717

@@ -20,6 +20,7 @@ public void runGenerator() {
2020
"--generator-name", "dart2-api",
2121
"--enable-post-process-file",
2222
"--additional-properties", "pubName=sample_app",
23+
// "--additional-properties", "disableCopyWith",
2324
// "--type-mappings", "int-or-string=IntOrString",
2425
// "--import-mappings", "IntOrString=./int_or_string.dart",
2526
// "--global-property", "skipFormModel=false",

src/test/resources/copy-with.yaml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
openapi: 3.0.1
2+
info:
3+
title: Copy-With problem issue 76
4+
version: 1.0.0
5+
paths:
6+
/inheritance:
7+
get:
8+
tags:
9+
- MapService
10+
operationId: "inheritance"
11+
responses:
12+
"200":
13+
description: "Description"
14+
content:
15+
application/json:
16+
schema:
17+
$ref: "#/components/schemas/SearchParams"
18+
components:
19+
schemas:
20+
SearchParams:
21+
type: object
22+
additionalProperties: false
23+
properties:
24+
phrase:
25+
type: string
26+
nullable: true
27+
virtualCategory:
28+
type: string
29+
nullable: true
30+
inDescription:
31+
type: boolean
32+
nullable: true
33+
default: false
34+
sorting:
35+
type: string
36+
nullable: true
37+
filters:
38+
type: object
39+
nullable: true
40+
default: {}
41+
additionalProperties:
42+
"$ref": "#/components/schemas/FilterQuery"
43+
categoryId:
44+
type: integer
45+
nullable: true
46+
default: 0
47+
slug:
48+
type: string
49+
nullable: true
50+
FilterQuery:
51+
type: object
52+
additionalProperties: false
53+
properties:
54+
values:
55+
type: string
56+
nullable: true
57+
from:
58+
type: number
59+
format: float
60+
nullable: true
61+
to:
62+
type: number
63+
format: float
64+
nullable: true

0 commit comments

Comments
 (0)