diff --git a/.gitignore b/.gitignore
index 466097b..2d4b255 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,14 @@
.DS_Store
-/.dart_tool
+.dart_tool
# FVM Version Cache
.fvm/
# Integration Test
-/integration_test/petstore/petstore_api
/integration_test/imposter.jar
+/integration_test/petstore/petstore_api
/integration_test/music_streaming/music_streaming_api
+/integration_test/gov/gov_api
+
+# Dependencies
+/pubspec.lock
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d181de6..2bb3ab4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,44 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## 2025-06-15
+
+### Changes
+
+---
+
+Packages with breaking changes:
+
+ - There are no breaking changes in this release.
+
+Packages with other changes:
+
+ - [`tonik` - `v0.0.6`](#tonik---v006)
+ - [`tonik_core` - `v0.0.6`](#tonik_core---v006)
+ - [`tonik_generate` - `v0.0.6`](#tonik_generate---v006)
+ - [`tonik_parse` - `v0.0.6`](#tonik_parse---v006)
+ - [`tonik_util` - `v0.0.6`](#tonik_util---v006)
+
+---
+
+#### `tonik` - `v0.0.6`
+
+#### `tonik_core` - `v0.0.6`
+
+#### `tonik_generate` - `v0.0.6`
+
+ - **FIX**: proper handle dates.
+ - **FIX**: priority for exlict defined names of schemas.
+ - **FIX**: prio for explicitly defined names.
+ - **FIX**: proper hash code for classes with >20 properties.
+
+#### `tonik_parse` - `v0.0.6`
+
+#### `tonik_util` - `v0.0.6`
+
+ - **FIX**: proper handle dates.
+
+
## 2025-06-02
### Changes
diff --git a/integration_test/gov/gov_test/.gitignore b/integration_test/gov/gov_test/.gitignore
new file mode 100644
index 0000000..3cceda5
--- /dev/null
+++ b/integration_test/gov/gov_test/.gitignore
@@ -0,0 +1,7 @@
+# https://dart.dev/guides/libraries/private-files
+# Created by `dart pub`
+.dart_tool/
+
+# Avoid committing pubspec.lock for library packages; see
+# https://dart.dev/guides/libraries/private-files#pubspeclock.
+pubspec.lock
diff --git a/integration_test/gov/gov_test/analysis_options.yaml b/integration_test/gov/gov_test/analysis_options.yaml
new file mode 100644
index 0000000..dee8927
--- /dev/null
+++ b/integration_test/gov/gov_test/analysis_options.yaml
@@ -0,0 +1,30 @@
+# This file configures the static analysis results for your project (errors,
+# warnings, and lints).
+#
+# This enables the 'recommended' set of lints from `package:lints`.
+# This set helps identify many issues that may lead to problems when running
+# or consuming Dart code, and enforces writing Dart using a single, idiomatic
+# style and format.
+#
+# If you want a smaller set of lints you can change this to specify
+# 'package:lints/core.yaml'. These are just the most critical lints
+# (the recommended set includes the core lints).
+# The core lints are also what is used by pub.dev for scoring packages.
+
+include: package:lints/recommended.yaml
+
+# Uncomment the following section to specify additional rules.
+
+# linter:
+# rules:
+# - camel_case_types
+
+# analyzer:
+# exclude:
+# - path/to/excluded/files/**
+
+# For more information about the core and recommended set of lints, see
+# https://dart.dev/go/core-lints
+
+# For additional information about configuring this file, see
+# https://dart.dev/guides/language/analysis-options
diff --git a/integration_test/gov/gov_test/imposter/imposter-config.json b/integration_test/gov/gov_test/imposter/imposter-config.json
new file mode 100644
index 0000000..a2480b3
--- /dev/null
+++ b/integration_test/gov/gov_test/imposter/imposter-config.json
@@ -0,0 +1,7 @@
+{
+ "plugin": "openapi",
+ "specFile": "../../openapi.yaml",
+ "response": {
+ "scriptFile": "response.groovy"
+ }
+}
\ No newline at end of file
diff --git a/integration_test/gov/gov_test/imposter/response.groovy b/integration_test/gov/gov_test/imposter/response.groovy
new file mode 100644
index 0000000..65422ea
--- /dev/null
+++ b/integration_test/gov/gov_test/imposter/response.groovy
@@ -0,0 +1,7 @@
+// Get the response status from the request header
+def responseStatus = context.request.headers['X-Response-Status'] ?: '200'
+
+// Set the response status code and use the OpenAPI specification
+respond()
+ .withStatusCode(Integer.parseInt(responseStatus))
+ .usingDefaultBehaviour()
diff --git a/integration_test/gov/gov_test/pubspec.yaml b/integration_test/gov/gov_test/pubspec.yaml
new file mode 100644
index 0000000..b4c67f7
--- /dev/null
+++ b/integration_test/gov/gov_test/pubspec.yaml
@@ -0,0 +1,19 @@
+name: gov_test
+description: A starting point for Dart libraries or applications.
+version: 1.0.0
+publish_to: none
+
+environment:
+ sdk: ^3.8.0
+
+dependencies:
+ dio: ^5.8.0
+ gov_api:
+ path: ../gov_api
+ path: ^1.8.3
+ tonik_util: ^0.0.6
+
+dev_dependencies:
+ test: ^1.24.0
+ very_good_analysis: ^9.0.0
+
diff --git a/integration_test/gov/gov_test/test/default_test.dart b/integration_test/gov/gov_test/test/default_test.dart
new file mode 100644
index 0000000..59efdeb
--- /dev/null
+++ b/integration_test/gov/gov_test/test/default_test.dart
@@ -0,0 +1,120 @@
+import 'package:dio/dio.dart';
+import 'package:gov_api/gov_api.dart';
+import 'package:test/test.dart';
+import 'package:tonik_util/tonik_util.dart';
+
+import 'test_helper.dart';
+
+void main() {
+ const port = 8080;
+ const baseUrl = 'http://localhost:$port';
+
+ late ImposterServer imposterServer;
+
+ setUpAll(() async {
+ imposterServer = ImposterServer(port: port);
+ await setupImposterServer(imposterServer);
+ });
+
+ DefaultApi buildAlbumsApi({required String responseStatus}) {
+ return DefaultApi(
+ CustomServer(
+ baseUrl: baseUrl,
+ serverConfig: ServerConfig(
+ baseOptions: BaseOptions(
+ headers: {'X-Response-Status': responseStatus},
+ ),
+ ),
+ ),
+ );
+ }
+
+ group('findForms', () {
+ test('200', () async {
+ final defaultApi = buildAlbumsApi(responseStatus: '200');
+
+ final response = await defaultApi.findForms(query: '10-10EZ');
+
+ expect(response, isA Use VA Form 10-10EZ if you’re a Veteran and want to apply for VA health care. You must be enrolled in... Use VA Form 10-10EZ if you’re a Veteran and want to apply for VA health care. You must be enrolled in...>());
+
+ final formIndex = body.data.first;
+ expect(formIndex.attributes?.benefitCategories, isA
>());
+ expect(formIndex.id, isA
?>());
+ expect(attributes?.sha256, isA
>());
+ expect(albumBase.externalUrls, isA
>());
+ expect(albumBase.name, isA
?>());
+ final artist = albumObject.artists?.first;
+ expect(artist?.externalUrls, isA
>(),
+ );
+ final trackItem = track?.pagingSimplifiedTrackObjectModel.items?.first;
+ expect(trackItem?.artists, isA
?>());
+ expect(trackItem?.availableMarkets, isA
?>());
+ expect(trackItem?.discNumber, isA
?>());
+ final copyright = albumObject.copyrights?.first;
+ expect(copyright?.text, isA
?>());
+ expect(albumObject.label, isA
-
+
+
+
[
Code("final map = json.decodeMap(context: '$className');"),
];
diff --git a/packages/tonik_generate/lib/src/model/one_of_generator.dart b/packages/tonik_generate/lib/src/model/one_of_generator.dart
index 06976db..6ec9beb 100644
--- a/packages/tonik_generate/lib/src/model/one_of_generator.dart
+++ b/packages/tonik_generate/lib/src/model/one_of_generator.dart
@@ -366,8 +366,9 @@ class OneOfGenerator {
// Throw if no match found.
blocks.add(
- generateJsonDecodingExceptionExpression('Invalid JSON for $className')
- .statement,
+ generateJsonDecodingExceptionExpression(
+ 'Invalid JSON for $className',
+ ).statement,
);
return Block.of(blocks);
diff --git a/packages/tonik_generate/lib/src/naming/name_manager.dart b/packages/tonik_generate/lib/src/naming/name_manager.dart
index aadbe2d..a3c7671 100644
--- a/packages/tonik_generate/lib/src/naming/name_manager.dart
+++ b/packages/tonik_generate/lib/src/naming/name_manager.dart
@@ -58,7 +58,16 @@ class NameManager {
_logServerName(entry.value, entry.key);
}
- for (final model in models) {
+ for (final model in models.where(
+ (m) => m is NamedModel && m.name != null,
+ )) {
+ final name = modelName(model);
+ _logModelName(name, model);
+ }
+
+ for (final model in models.where(
+ (m) => m is! NamedModel || m.name == null,
+ )) {
final name = modelName(model);
_logModelName(name, model);
}
diff --git a/packages/tonik_generate/lib/src/operation/parse_generator.dart b/packages/tonik_generate/lib/src/operation/parse_generator.dart
index 99d45a1..0b7fb9a 100644
--- a/packages/tonik_generate/lib/src/operation/parse_generator.dart
+++ b/packages/tonik_generate/lib/src/operation/parse_generator.dart
@@ -16,11 +16,12 @@ class ParseGenerator {
/// Generates the _parseResponse method for the operation.
Method generateParseResponseMethod(Operation operation) {
final responses = operation.responses;
- final responseType = resultTypeForOperation(
- operation,
- nameManager,
- package,
- ).types.first;
+ final responseType =
+ resultTypeForOperation(
+ operation,
+ nameManager,
+ package,
+ ).types.first;
final cases = [];
// Check if we have a default response with null content type
@@ -49,7 +50,7 @@ class ParseGenerator {
}
}
- // Only add a default case if we don't have a default response with
+ // Only add a default case if we don't have a default response with
// null content type
final switchCases = [
const Code(
@@ -60,17 +61,19 @@ class ParseGenerator {
];
if (!hasDefaultWithNullContentType) {
- switchCases.add(Block.of([
- const Code('default:'),
- const Code(
- "final content = response.headers.value('content-type') "
- "?? 'not specified';",
- ),
- const Code('final status = response.statusCode;'),
- generateDecodingExceptionExpression(
- r'Unexpected content type: $content for status code: $status',
- ).statement,
- ]),);
+ switchCases.add(
+ Block.of([
+ const Code('default:'),
+ const Code(
+ "final content = response.headers.value('content-type') "
+ "?? 'not specified';",
+ ),
+ const Code('final status = response.statusCode;'),
+ generateDecodingExceptionExpression(
+ r'Unexpected content type: $content for status code: $status',
+ ).statement,
+ ]),
+ );
}
switchCases.add(const Code('}'));
diff --git a/packages/tonik_generate/lib/src/pubspec_generator.dart b/packages/tonik_generate/lib/src/pubspec_generator.dart
index ef578fb..7bb991e 100644
--- a/packages/tonik_generate/lib/src/pubspec_generator.dart
+++ b/packages/tonik_generate/lib/src/pubspec_generator.dart
@@ -25,8 +25,9 @@ dependencies:
big_decimal: ^0.5.0
collection: ^1.17.0
dio: ^5.8.0+1
+ lints: ^6.0.0
meta: ^1.16.0
- tonik_util: ^0.0.5
+ tonik_util: ^0.0.6
''';
pubspecFile.writeAsStringSync(content);
diff --git a/packages/tonik_generate/lib/src/response_wrapper/response_wrapper_generator.dart b/packages/tonik_generate/lib/src/response_wrapper/response_wrapper_generator.dart
index 7cfc1b9..f5601d7 100644
--- a/packages/tonik_generate/lib/src/response_wrapper/response_wrapper_generator.dart
+++ b/packages/tonik_generate/lib/src/response_wrapper/response_wrapper_generator.dart
@@ -84,7 +84,7 @@ class ResponseWrapperGenerator {
(response.bodyCount > 1 || response.hasHeaders)) {
final responseClassName =
nameManager.responseNames(response.resolved).baseName;
-
+
bodyField = Field(
(b) =>
b
diff --git a/packages/tonik_generate/lib/src/server/server_file_generator.dart b/packages/tonik_generate/lib/src/server/server_file_generator.dart
index fcaefd8..488c832 100644
--- a/packages/tonik_generate/lib/src/server/server_file_generator.dart
+++ b/packages/tonik_generate/lib/src/server/server_file_generator.dart
@@ -40,4 +40,4 @@ class ServerFileGenerator {
final filePath = path.join(serverDirPath, result.filename);
File(filePath).writeAsStringSync(result.code);
}
-}
+}
diff --git a/packages/tonik_generate/lib/src/util/doc_comment_formatter.dart b/packages/tonik_generate/lib/src/util/doc_comment_formatter.dart
index 55e774d..c3860aa 100644
--- a/packages/tonik_generate/lib/src/util/doc_comment_formatter.dart
+++ b/packages/tonik_generate/lib/src/util/doc_comment_formatter.dart
@@ -1,18 +1,18 @@
/// Formats a single string as a doc comment.
-///
+///
/// If the string is multiline, each line will be prefixed with '/// '.
/// Returns an empty list if the input is null or empty.
List formatDocComment(String? text) {
if (text == null || text.isEmpty) {
return [];
}
-
+
// Split by newlines and prefix each line with '/// '
return text.split('\n').map((line) => '/// $line').toList();
}
/// Formats a list of strings as doc comments.
-///
+///
/// Each string in the list is processed with [formatDocComment],
/// and the results are flattened into a single list.
/// Null and empty strings are filtered out.
@@ -23,12 +23,12 @@ List formatDocComments(List? texts) {
}
final result = [];
-
+
for (final text in texts) {
if (text != null && text.isNotEmpty) {
result.addAll(formatDocComment(text));
}
}
-
+
return result;
-}
+}
diff --git a/packages/tonik_generate/lib/src/util/exception_code_generator.dart b/packages/tonik_generate/lib/src/util/exception_code_generator.dart
index 5580b9d..ec738fb 100644
--- a/packages/tonik_generate/lib/src/util/exception_code_generator.dart
+++ b/packages/tonik_generate/lib/src/util/exception_code_generator.dart
@@ -1,7 +1,5 @@
import 'package:code_builder/code_builder.dart';
-
-
/// Generates a throw expression for ArgumentError.
Expression generateArgumentErrorExpression(String message) {
return _generateExceptionExpression('ArgumentError', message);
diff --git a/packages/tonik_generate/lib/src/util/format_with_header.dart b/packages/tonik_generate/lib/src/util/format_with_header.dart
index 6a51ab5..d6f305d 100644
--- a/packages/tonik_generate/lib/src/util/format_with_header.dart
+++ b/packages/tonik_generate/lib/src/util/format_with_header.dart
@@ -1,19 +1,10 @@
import 'package:dart_style/dart_style.dart';
extension FormatWithHeader on DartFormatter {
- static const _ignores = [
- 'lines_longer_than_80_chars',
- 'unnecessary_raw_strings',
- 'unnecessary_brace_in_string_interps',
- 'no_leading_underscores_for_local_identifiers',
- 'cascade_invocations',
- 'prefer_is_empty',
- ];
-
String formatWithHeader(String code) {
return format('''
// Generated code - do not modify by hand
-${_ignores.map((i) => '// ignore_for_file: $i').join('\n')}
+
$code''');
}
}
diff --git a/packages/tonik_generate/lib/src/util/hash_code_generator.dart b/packages/tonik_generate/lib/src/util/hash_code_generator.dart
index e882b22..be1d733 100644
--- a/packages/tonik_generate/lib/src/util/hash_code_generator.dart
+++ b/packages/tonik_generate/lib/src/util/hash_code_generator.dart
@@ -80,7 +80,7 @@ Method generateHashCodeMethod({
refer(
'Object',
'dart:core',
- ).property('hash').call(hashArgs, {}, []).returned.statement,
+ ).property('hashAll').call([literalList(hashArgs)]).returned.statement,
);
}
diff --git a/packages/tonik_generate/lib/src/util/response_property_normalizer.dart b/packages/tonik_generate/lib/src/util/response_property_normalizer.dart
index 1b9c137..ea7ef4c 100644
--- a/packages/tonik_generate/lib/src/util/response_property_normalizer.dart
+++ b/packages/tonik_generate/lib/src/util/response_property_normalizer.dart
@@ -7,7 +7,6 @@ List<({String normalizedName, Property property, ResponseHeader? header})>
normalizeResponseProperties(ResponseObject response) {
final headerMap = {};
-
final headerProperties = response.headers.entries.map((header) {
final property = Property(
name:
diff --git a/packages/tonik_generate/lib/src/util/response_type_generator.dart b/packages/tonik_generate/lib/src/util/response_type_generator.dart
index 4666e0f..281898e 100644
--- a/packages/tonik_generate/lib/src/util/response_type_generator.dart
+++ b/packages/tonik_generate/lib/src/util/response_type_generator.dart
@@ -4,7 +4,7 @@ import 'package:tonik_core/tonik_core.dart';
import 'package:tonik_generate/src/naming/name_manager.dart';
import 'package:tonik_generate/src/util/type_reference_generator.dart';
-/// Generates the appropriate return type for an operation
+/// Generates the appropriate return type for an operation
/// based on its responses.
TypeReference resultTypeForOperation(
Operation operation,
@@ -63,4 +63,4 @@ TypeReference resultTypeForOperation(
),
),
};
-}
+}
diff --git a/packages/tonik_generate/lib/src/util/to_json_value_expression_generator.dart b/packages/tonik_generate/lib/src/util/to_json_value_expression_generator.dart
index eaf0927..7d0a55d 100644
--- a/packages/tonik_generate/lib/src/util/to_json_value_expression_generator.dart
+++ b/packages/tonik_generate/lib/src/util/to_json_value_expression_generator.dart
@@ -47,8 +47,9 @@ String? _getSerializationSuffix(Model model, bool isNullable) {
isNullable || (model is EnumModel && model.isNullable) ? '?' : '';
return switch (model) {
- DateTimeModel() || DateModel() => '$nullablePart.toIso8601String()',
+ DateTimeModel() => '$nullablePart.toIso8601String()',
DecimalModel() => '$nullablePart.toString()',
+ DateModel() ||
EnumModel() ||
ClassModel() ||
AllOfModel() ||
diff --git a/packages/tonik_generate/lib/src/util/type_reference_generator.dart b/packages/tonik_generate/lib/src/util/type_reference_generator.dart
index 9be2bb4..63d6e85 100644
--- a/packages/tonik_generate/lib/src/util/type_reference_generator.dart
+++ b/packages/tonik_generate/lib/src/util/type_reference_generator.dart
@@ -71,8 +71,8 @@ TypeReference typeReference(
DateModel _ => TypeReference(
(b) =>
b
- ..symbol = 'DateTime'
- ..url = 'dart:core'
+ ..symbol = 'Date'
+ ..url = 'package:tonik_util/tonik_util.dart'
..isNullable = isNullableOverride,
),
DecimalModel _ => TypeReference(
@@ -87,8 +87,7 @@ TypeReference typeReference(
b
..symbol = nameManager.modelName(m)
..url = package
- ..isNullable =
- isNullableOverride,
+ ..isNullable = isNullableOverride,
),
};
}
diff --git a/packages/tonik_generate/pubspec.yaml b/packages/tonik_generate/pubspec.yaml
index 0ab9a31..ef81ba1 100644
--- a/packages/tonik_generate/pubspec.yaml
+++ b/packages/tonik_generate/pubspec.yaml
@@ -1,6 +1,6 @@
name: tonik_generate
description: A code generation package for Tonik.
-version: 0.0.5
+version: 0.0.6
repository: https://github.com/t-unit/tonik
resolution: workspace
@@ -16,8 +16,7 @@ dependencies:
meta: ^1.16.0
path: ^1.9.1
spell_out_numbers: ^1.0.0
- tonik_core: ^0.0.5
+ tonik_core: ^0.0.6
dev_dependencies:
test: ^1.24.0
- very_good_analysis: ^8.0.0
diff --git a/packages/tonik_generate/test/src/api_client/api_client_file_generator_test.dart b/packages/tonik_generate/test/src/api_client/api_client_file_generator_test.dart
index 7d69fed..379f41d 100644
--- a/packages/tonik_generate/test/src/api_client/api_client_file_generator_test.dart
+++ b/packages/tonik_generate/test/src/api_client/api_client_file_generator_test.dart
@@ -215,7 +215,7 @@ void main() {
// Read the generated file to verify it contains the operation
final fileContent =
File(clientDir.listSync().first.path).readAsStringSync();
-
+
expect(fileContent, contains('untaggedOperation'));
expect(fileContent, contains('class DefaultApi'));
});
diff --git a/packages/tonik_generate/test/src/model/all_of_generator_test.dart b/packages/tonik_generate/test/src/model/all_of_generator_test.dart
index 4c9030e..1a67894 100644
--- a/packages/tonik_generate/test/src/model/all_of_generator_test.dart
+++ b/packages/tonik_generate/test/src/model/all_of_generator_test.dart
@@ -327,7 +327,7 @@ void main() {
const expectedHashCode = '''
@override
int get hashCode {
- return Object.hash(base, mixin);
+ return Object.hashAll([base, mixin]);
}
''';
@@ -472,7 +472,7 @@ void main() {
const expectedHashCode = '''
@override
int get hashCode {
- return Object.hash(status, string);
+ return Object.hashAll([status, string]);
}
''';
@@ -501,14 +501,14 @@ void main() {
expect(combinedClass.fields, hasLength(2));
expect(
combinedClass.fields.map((f) => f.name),
- containsAll(['dateTime', 'string']),
+ containsAll(['date', 'string']),
);
// Check field types
final dateField = combinedClass.fields.firstWhere(
- (f) => f.name == 'dateTime',
+ (f) => f.name == 'date',
);
- expect(dateField.type?.accept(emitter).toString(), 'DateTime');
+ expect(dateField.type?.accept(emitter).toString(), 'Date');
final stringField = combinedClass.fields.firstWhere(
(f) => f.name == 'string',
@@ -517,7 +517,7 @@ void main() {
// Check toJson - should return the ISO string
const expectedToJson = '''
- Object? toJson() => dateTime.toIso8601String();
+ Object? toJson() => date.toJson();
''';
expect(
@@ -529,7 +529,7 @@ void main() {
const expectedFromJson = '''
factory DateStringModel.fromJson(Object? json) {
return DateStringModel(
- dateTime: json.decodeJsonDate(context: r'DateStringModel'),
+ date: json.decodeJsonDate(context: r'DateStringModel'),
string: json.decodeJsonString(context: r'DateStringModel'),
);
}
diff --git a/packages/tonik_generate/test/src/model/class_hash_code_generator_test.dart b/packages/tonik_generate/test/src/model/class_hash_code_generator_test.dart
index f3ba80b..588fda2 100644
--- a/packages/tonik_generate/test/src/model/class_hash_code_generator_test.dart
+++ b/packages/tonik_generate/test/src/model/class_hash_code_generator_test.dart
@@ -54,7 +54,7 @@ void main() {
const expectedMethod = '''
@override
- int get hashCode { return Object.hash(name, age); }
+ int get hashCode { return Object.hashAll([name, age]); }
''';
final generatedClass = generator.generateClass(model);
@@ -102,7 +102,7 @@ void main() {
const expectedMethod = '''
@override
- int get hashCode { return Object.hash(id, name, email, age); }
+ int get hashCode { return Object.hashAll([id, name, email, age]); }
''';
final generatedClass = generator.generateClass(model);
@@ -136,7 +136,7 @@ void main() {
const expectedMethod = '''
@override
- int get hashCode { return Object.hash(name, bio); }
+ int get hashCode { return Object.hashAll([name, bio]); }
''';
final generatedClass = generator.generateClass(model);
@@ -170,7 +170,7 @@ void main() {
const expectedMethod = '''
@override
- int get hashCode { return Object.hash(firstName, lastName); }
+ int get hashCode { return Object.hashAll([firstName, lastName]); }
''';
final generatedClass = generator.generateClass(model);
@@ -209,7 +209,7 @@ void main() {
@override
int get hashCode {
const deepEquals = DeepCollectionEquality();
- return Object.hash(name, deepEquals.hash(tags));
+ return Object.hashAll([name, deepEquals.hash(tags)]);
}
''';
@@ -252,7 +252,7 @@ void main() {
@override
int get hashCode {
const deepEquals = DeepCollectionEquality();
- return Object.hash(name, deepEquals.hash(nestedList));
+ return Object.hashAll([name, deepEquals.hash(nestedList)]);
}
''';
diff --git a/packages/tonik_generate/test/src/model/class_json_generator_test.dart b/packages/tonik_generate/test/src/model/class_json_generator_test.dart
index 2b391bb..7fb5505 100644
--- a/packages/tonik_generate/test/src/model/class_json_generator_test.dart
+++ b/packages/tonik_generate/test/src/model/class_json_generator_test.dart
@@ -597,5 +597,43 @@ void main() {
contains(collapseWhitespace(expectedMethod)),
);
});
+
+ test('generates fromJson method for class without properties', () {
+ final model = ClassModel(
+ context: context,
+ name: 'EmptyClass',
+ properties: const [],
+ );
+
+ const expectedMethod = '''
+ factory EmptyClass.fromJson(Object? json) {
+ return EmptyClass();
+ }''';
+
+ final generatedClass = generator.generateClass(model);
+ expect(
+ collapseWhitespace(format(generatedClass.accept(emitter).toString())),
+ contains(collapseWhitespace(expectedMethod)),
+ );
+ });
+
+ test('generates fromSimple method for class without properties', () {
+ final model = ClassModel(
+ context: context,
+ name: 'EmptyClass',
+ properties: const [],
+ );
+
+ const expectedMethod = '''
+ factory EmptyClass.fromSimple(String? value) {
+ return EmptyClass();
+ }''';
+
+ final generatedClass = generator.generateClass(model);
+ expect(
+ collapseWhitespace(format(generatedClass.accept(emitter).toString())),
+ contains(collapseWhitespace(expectedMethod)),
+ );
+ });
});
}
diff --git a/packages/tonik_generate/test/src/model/class_simple_generator_test.dart b/packages/tonik_generate/test/src/model/class_simple_generator_test.dart
index 420155c..8189a6a 100644
--- a/packages/tonik_generate/test/src/model/class_simple_generator_test.dart
+++ b/packages/tonik_generate/test/src/model/class_simple_generator_test.dart
@@ -232,7 +232,7 @@ void main() {
);
}
''';
-
+
expect(
collapseWhitespace(classCode),
contains(collapseWhitespace(expectedMethod)),
@@ -355,7 +355,7 @@ void main() {
);
}
''';
-
+
expect(
collapseWhitespace(classCode),
contains(collapseWhitespace(expectedMethod)),
diff --git a/packages/tonik_generate/test/src/model/typedef_generator_test.dart b/packages/tonik_generate/test/src/model/typedef_generator_test.dart
index fe4f88c..4d9d270 100644
--- a/packages/tonik_generate/test/src/model/typedef_generator_test.dart
+++ b/packages/tonik_generate/test/src/model/typedef_generator_test.dart
@@ -113,7 +113,7 @@ void main() {
(model: NumberModel(context: context), expectedType: 'num'),
(model: BooleanModel(context: context), expectedType: 'bool'),
(model: DateTimeModel(context: context), expectedType: 'DateTime'),
- (model: DateModel(context: context), expectedType: 'DateTime'),
+ (model: DateModel(context: context), expectedType: 'Date'),
(model: DecimalModel(context: context), expectedType: 'BigDecimal'),
];
diff --git a/packages/tonik_generate/test/src/naming/name_generator_test.dart b/packages/tonik_generate/test/src/naming/name_generator_test.dart
index b2c3858..8e2e473 100644
--- a/packages/tonik_generate/test/src/naming/name_generator_test.dart
+++ b/packages/tonik_generate/test/src/naming/name_generator_test.dart
@@ -1086,7 +1086,10 @@ void main() {
final generator = NameGenerator();
final servers = [
const Server(url: 'https://api.dev.example.com', description: null),
- const Server(url: 'https://api.staging.example.com', description: null),
+ const Server(
+ url: 'https://api.staging.example.com',
+ description: null,
+ ),
const Server(url: 'https://api.prod.example.com', description: null),
];
@@ -1178,7 +1181,8 @@ void main() {
expect(result.serverMap[servers[0]], 'CustomServer');
expect(result.customName, r'CustomServer$');
expect(result.baseName, 'ApiServer');
- });
+ },
+ );
test('uses default names on invalid URLs', () {
final generator = NameGenerator();
diff --git a/packages/tonik_generate/test/src/naming/name_manager_test.dart b/packages/tonik_generate/test/src/naming/name_manager_test.dart
index 4102156..2eb923e 100644
--- a/packages/tonik_generate/test/src/naming/name_manager_test.dart
+++ b/packages/tonik_generate/test/src/naming/name_manager_test.dart
@@ -599,6 +599,70 @@ void main() {
expect(baseName2, baseName);
expect(identical(implementationNames, implementationNames2), isTrue);
});
+
+ group('model naming behavior', () {
+ late Context userContext;
+
+ setUp(() {
+ userContext = Context.initial().pushAll([
+ 'components',
+ 'schemas',
+ 'user',
+ ]);
+ });
+
+ test(
+ 'named model keeps original name and anonymous model gets Model suffix',
+ () {
+ final models = [
+ ClassModel(
+ name: 'User',
+ properties: const [],
+ context: userContext,
+ ),
+ ClassModel(properties: const [], context: userContext),
+ ];
+
+ manager.prime(
+ models: models,
+ responses: const [],
+ operations: const [],
+ tags: const [],
+ requestBodies: const [],
+ servers: const [],
+ );
+
+ expect(manager.modelName(models[0]), 'User');
+ expect(manager.modelName(models[1]), 'UserModel');
+ },
+ );
+
+ test(
+ 'named model takes precedence over anonymous model with same context',
+ () {
+ final models = [
+ ClassModel(properties: const [], context: userContext),
+ ClassModel(
+ name: 'User',
+ properties: const [],
+ context: userContext,
+ ),
+ ];
+
+ manager.prime(
+ models: models,
+ responses: const [],
+ operations: const [],
+ tags: const [],
+ requestBodies: const [],
+ servers: const [],
+ );
+
+ expect(manager.modelName(models[0]), 'UserModel');
+ expect(manager.modelName(models[1]), 'User');
+ },
+ );
+ });
});
group('Server names with list-based caching', () {
@@ -626,10 +690,10 @@ void main() {
const Server(url: 'https://staging.example.com', description: null),
const Server(url: 'https://dev.example.com', description: null),
];
-
+
// Identity should be different but content equal
expect(identical(servers, identicalContentServers), isFalse);
-
+
// Second call with different list but same content should use cache
final result2 = manager.serverNames(identicalContentServers);
@@ -637,18 +701,18 @@ void main() {
expect(result1.serverMap.length, result2.serverMap.length);
expect(result1.baseName, result2.baseName);
expect(result1.customName, result2.customName);
-
+
// The cache should only have one entry despite using two different lists
expect(manager.serverNamesCache.length, 1);
-
+
// Check that corresponding servers in each list have the same names
for (var i = 0; i < servers.length; i++) {
final server1 = servers[i];
final server2 = identicalContentServers[i];
-
+
final name1 = result1.serverMap[server1];
final name2 = result2.serverMap[server2];
-
+
expect(name1, name2);
}
});
@@ -674,28 +738,28 @@ void main() {
// Verify the server names are cached
expect(manager.serverNamesCache.length, 1);
-
+
// Verify the cache contains the correct key
expect(manager.serverNamesCache.containsKey(cacheKey), isTrue);
-
+
// Get the cached result
final cachedResult = manager.serverNamesCache[cacheKey]!;
-
+
// Verify the cached result has correct server map size
expect(cachedResult.serverMap.length, 2);
-
+
// Check that the servers are properly mapped to their expected names
for (final server in servers) {
final name = cachedResult.serverMap[server];
expect(name != null, isTrue);
-
+
if (server.url == 'https://api.example.com') {
expect(name!.startsWith('Api'), isTrue);
} else if (server.url == 'https://staging.example.com') {
expect(name!.startsWith('Staging'), isTrue);
}
}
-
+
// Verify custom name exists
expect(cachedResult.customName.contains('Custom'), isTrue);
});
diff --git a/packages/tonik_generate/test/src/operation/data_generator_test.dart b/packages/tonik_generate/test/src/operation/data_generator_test.dart
index 284bfd5..66c8e16 100644
--- a/packages/tonik_generate/test/src/operation/data_generator_test.dart
+++ b/packages/tonik_generate/test/src/operation/data_generator_test.dart
@@ -239,8 +239,8 @@ void main() {
);
const expectedMethod = '''
- Object? _data({required DateTime? body}) {
- return body?.toIso8601String();
+ Object? _data({required Date? body}) {
+ return body?.toJson();
}
''';
diff --git a/packages/tonik_generate/test/src/operation/operation_generator_response_test.dart b/packages/tonik_generate/test/src/operation/operation_generator_response_test.dart
index e9eba7b..b3b8c0b 100644
--- a/packages/tonik_generate/test/src/operation/operation_generator_response_test.dart
+++ b/packages/tonik_generate/test/src/operation/operation_generator_response_test.dart
@@ -169,49 +169,47 @@ void main() {
);
});
- test(
- 'returns result with model for single status code with body only',
- () {
- final operation = Operation(
- operationId: 'bodyStatus',
- context: context,
- summary: '',
- description: '',
- tags: const {},
- isDeprecated: false,
- path: '/body',
- method: HttpMethod.get,
- headers: const {},
- queryParameters: const {},
- pathParameters: const {},
- requestBody: null,
- responses: {
- const ExplicitResponseStatus(statusCode: 200): ResponseObject(
- name: 'BodyResponse',
- context: context,
- headers: const {},
- description: '',
- bodies: {
- ResponseBody(
- model: StringModel(context: context),
- rawContentType: 'application/json',
- contentType: ContentType.json,
- ),
- },
- ),
- },
- );
- const normalizedParams = NormalizedRequestParameters(
- pathParameters: [],
- queryParameters: [],
- headers: [],
- );
- final method = generator.generateCallMethod(
- operation,
- normalizedParams,
- );
- expect(
- method.returns?.accept(emitter).toString(),
+ test('returns result with model for single status code with body only', () {
+ final operation = Operation(
+ operationId: 'bodyStatus',
+ context: context,
+ summary: '',
+ description: '',
+ tags: const {},
+ isDeprecated: false,
+ path: '/body',
+ method: HttpMethod.get,
+ headers: const {},
+ queryParameters: const {},
+ pathParameters: const {},
+ requestBody: null,
+ responses: {
+ const ExplicitResponseStatus(statusCode: 200): ResponseObject(
+ name: 'BodyResponse',
+ context: context,
+ headers: const {},
+ description: '',
+ bodies: {
+ ResponseBody(
+ model: StringModel(context: context),
+ rawContentType: 'application/json',
+ contentType: ContentType.json,
+ ),
+ },
+ ),
+ },
+ );
+ const normalizedParams = NormalizedRequestParameters(
+ pathParameters: [],
+ queryParameters: [],
+ headers: [],
+ );
+ final method = generator.generateCallMethod(
+ operation,
+ normalizedParams,
+ );
+ expect(
+ method.returns?.accept(emitter).toString(),
'Future>',
);
});
diff --git a/packages/tonik_generate/test/src/response/response_class_generator_test.dart b/packages/tonik_generate/test/src/response/response_class_generator_test.dart
index ca46a52..ae48ec6 100644
--- a/packages/tonik_generate/test/src/response/response_class_generator_test.dart
+++ b/packages/tonik_generate/test/src/response/response_class_generator_test.dart
@@ -311,7 +311,7 @@ void main() {
const expectedMethod = '''
@override
int get hashCode {
- return Object.hash(xTest, body);
+ return Object.hashAll([xTest, body]);
}
''';
@@ -361,7 +361,7 @@ void main() {
const expectedMethod = '''
@override
int get hashCode {
- return Object.hash(xTest, body, xOther);
+ return Object.hashAll([xTest, body, xOther]);
}
''';
@@ -406,7 +406,7 @@ void main() {
@override
int get hashCode {
const deepEquals = DeepCollectionEquality();
- return Object.hash(deepEquals.hash(xList), body);
+ return Object.hashAll([deepEquals.hash(xList), body]);
}
''';
diff --git a/packages/tonik_generate/test/src/server/server_file_generator_test.dart b/packages/tonik_generate/test/src/server/server_file_generator_test.dart
index 8b1b600..580bd63 100644
--- a/packages/tonik_generate/test/src/server/server_file_generator_test.dart
+++ b/packages/tonik_generate/test/src/server/server_file_generator_test.dart
@@ -100,17 +100,17 @@ void main() {
final serverDir = Directory(
path.join(tempDir.path, 'test_package', 'lib', 'src', 'server'),
);
-
+
expect(serverDir.listSync(), hasLength(1));
-
+
// Get file name and content
final generatedFile = serverDir.listSync().first;
final actualFileName = path.basename(generatedFile.path);
final fileContent = File(generatedFile.path).readAsStringSync();
-
+
// Check file name
expect(actualFileName, equals('api_server.dart'));
-
+
// Check file content
expect(fileContent, contains('sealed class ApiServer'));
expect(fileContent, contains('class ProductionServer'));
@@ -128,7 +128,7 @@ void main() {
models: {},
responseHeaders: {},
requestHeaders: {},
- servers: {}, // Empty servers collection
+ servers: {}, // Empty servers collection
operations: {},
responses: {},
queryParameters: {},
@@ -148,17 +148,17 @@ void main() {
);
expect(serverDir.existsSync(), isTrue);
expect(serverDir.listSync(), hasLength(1));
-
+
// Get file content
final generatedFile = serverDir.listSync().first;
final fileContent = File(generatedFile.path).readAsStringSync();
-
+
// Expect base class and custom class to be generated
expect(fileContent, contains('sealed class ApiServer'));
expect(fileContent, contains('class CustomServer'));
-
+
// No server-specific classes should be present
expect(fileContent.split('class').length, 3);
});
});
-}
+}
diff --git a/packages/tonik_generate/test/src/server/server_generator_test.dart b/packages/tonik_generate/test/src/server/server_generator_test.dart
index 77c768c..8f32742 100644
--- a/packages/tonik_generate/test/src/server/server_generator_test.dart
+++ b/packages/tonik_generate/test/src/server/server_generator_test.dart
@@ -65,7 +65,7 @@ void main() {
test('generates constructor with named parameters', () {
final constructor = baseClass.constructors.first;
- // Constructor should not be const since _dio is not final
+ // Constructor should not be const since _dio is not final
// and initialized later
expect(constructor.constant, isFalse);
expect(constructor.optionalParameters.length, 2);
@@ -121,7 +121,7 @@ void main() {
final productionClass = generatedClasses[1];
final productionConstructor = productionClass.constructors.first;
- // Constructor should not be const since base class
+ // Constructor should not be const since base class
// constructor isn't const
expect(productionConstructor.constant, isFalse);
expect(productionConstructor.optionalParameters.length, 1);
@@ -155,7 +155,7 @@ void main() {
final stagingClass = generatedClasses[2];
final stagingConstructor = stagingClass.constructors.first;
- // Constructor should not be const since base class constructor
+ // Constructor should not be const since base class constructor
// isn't const
expect(stagingConstructor.constant, isFalse);
expect(stagingConstructor.optionalParameters.length, 1);
@@ -191,7 +191,7 @@ void main() {
final customClass = generatedClasses.last;
final customConstructor = customClass.constructors.first;
- // Constructor should not be const since base class constructor
+ // Constructor should not be const since base class constructor
// isn't const
expect(customConstructor.constant, isFalse);
expect(customConstructor.optionalParameters.length, 2);
diff --git a/packages/tonik_generate/test/src/util/doc_comment_formatter_test.dart b/packages/tonik_generate/test/src/util/doc_comment_formatter_test.dart
index bc9d990..8f7452b 100644
--- a/packages/tonik_generate/test/src/util/doc_comment_formatter_test.dart
+++ b/packages/tonik_generate/test/src/util/doc_comment_formatter_test.dart
@@ -5,7 +5,7 @@ void main() {
group('doc comment formatter', () {
test('formats a single line string', () {
final result = formatDocComment('This is a doc comment');
-
+
expect(result, isNotEmpty);
expect(result.length, 1);
expect(result.first, '/// This is a doc comment');
@@ -15,7 +15,7 @@ void main() {
final result = formatDocComment(
'This is a multiline\ndoc comment\nwith three lines',
);
-
+
expect(result, isNotEmpty);
expect(result.length, 3);
expect(result[0], '/// This is a multiline');
@@ -25,13 +25,13 @@ void main() {
test('returns empty list for null string', () {
final result = formatDocComment(null);
-
+
expect(result, isEmpty);
});
test('returns empty list for empty string', () {
final result = formatDocComment('');
-
+
expect(result, isEmpty);
});
@@ -43,7 +43,7 @@ void main() {
'',
'Last line',
]);
-
+
expect(result, isNotEmpty);
expect(result.length, 3);
expect(result[0], '/// First line');
@@ -53,19 +53,19 @@ void main() {
test('returns empty list for null list', () {
final result = formatDocComments(null);
-
+
expect(result, isEmpty);
});
test('returns empty list for empty list', () {
final result = formatDocComments([]);
-
+
expect(result, isEmpty);
});
test('returns empty list for list of nulls and empty strings', () {
final result = formatDocComments([null, '', null]);
-
+
expect(result, isEmpty);
});
@@ -74,7 +74,7 @@ void main() {
'First\nMultiline',
'Second',
]);
-
+
expect(result, isNotEmpty);
expect(result.length, 3);
expect(result[0], '/// First');
@@ -82,4 +82,4 @@ void main() {
expect(result[2], '/// Second');
});
});
-}
+}
diff --git a/packages/tonik_generate/test/src/util/from_json_value_expression_generator_test.dart b/packages/tonik_generate/test/src/util/from_json_value_expression_generator_test.dart
index 4fab605..ee76425 100644
--- a/packages/tonik_generate/test/src/util/from_json_value_expression_generator_test.dart
+++ b/packages/tonik_generate/test/src/util/from_json_value_expression_generator_test.dart
@@ -514,14 +514,15 @@ void main() {
context: context,
);
- final expresion = buildFromJsonValueExpression(
- 'value',
- model: nestedListModel,
- nameManager: nameManager,
- package: 'package:my_package/my_package.dart',
- contextClass: 'Order',
- contextProperty: 'items',
- ).accept(emitter).toString();
+ final expresion =
+ buildFromJsonValueExpression(
+ 'value',
+ model: nestedListModel,
+ nameManager: nameManager,
+ package: 'package:my_package/my_package.dart',
+ contextClass: 'Order',
+ contextProperty: 'items',
+ ).accept(emitter).toString();
expect(
expresion,
diff --git a/packages/tonik_generate/test/src/util/from_simple_value_expression_generator_test.dart b/packages/tonik_generate/test/src/util/from_simple_value_expression_generator_test.dart
index 9aea25b..244a69a 100644
--- a/packages/tonik_generate/test/src/util/from_simple_value_expression_generator_test.dart
+++ b/packages/tonik_generate/test/src/util/from_simple_value_expression_generator_test.dart
@@ -296,7 +296,7 @@ void main() {
test('passes context parameter to decode methods when provided', () {
final value = refer('value');
-
+
expect(
buildSimpleValueExpression(
value,
diff --git a/packages/tonik_generate/test/src/util/hash_code_generator_test.dart b/packages/tonik_generate/test/src/util/hash_code_generator_test.dart
index ade3fbd..1f773a8 100644
--- a/packages/tonik_generate/test/src/util/hash_code_generator_test.dart
+++ b/packages/tonik_generate/test/src/util/hash_code_generator_test.dart
@@ -95,7 +95,58 @@ void main() {
@override
int get hashCode {
const deepEquals = DeepCollectionEquality();
- return Object.hash(id, deepEquals.hash(items), name);
+ return Object.hashAll([id, deepEquals.hash(items), name]);
+ }
+ ''';
+
+ expect(
+ collapseWhitespace(formatMethod(method)),
+ contains(collapseWhitespace(expectedMethod)),
+ );
+ });
+
+ test('generates hash code method for class with many properties', () {
+ final method = generateHashCodeMethod(
+ properties: List.generate(
+ 25,
+ (i) => (
+ normalizedName: 'prop$i',
+ hasCollectionValue: i.isEven,
+ ),
+ ),
+ );
+
+ const expectedMethod = '''
+ @override
+ int get hashCode {
+ const deepEquals = DeepCollectionEquality();
+ return Object.hashAll([
+ deepEquals.hash(prop0),
+ prop1,
+ deepEquals.hash(prop2),
+ prop3,
+ deepEquals.hash(prop4),
+ prop5,
+ deepEquals.hash(prop6),
+ prop7,
+ deepEquals.hash(prop8),
+ prop9,
+ deepEquals.hash(prop10),
+ prop11,
+ deepEquals.hash(prop12),
+ prop13,
+ deepEquals.hash(prop14),
+ prop15,
+ deepEquals.hash(prop16),
+ prop17,
+ deepEquals.hash(prop18),
+ prop19,
+ deepEquals.hash(prop20),
+ prop21,
+ deepEquals.hash(prop22),
+ prop23,
+ deepEquals.hash(prop24),
+ ]);
}
''';
diff --git a/packages/tonik_generate/test/src/util/to_json_value_expression_generator_test.dart b/packages/tonik_generate/test/src/util/to_json_value_expression_generator_test.dart
index 4f48260..f54b21a 100644
--- a/packages/tonik_generate/test/src/util/to_json_value_expression_generator_test.dart
+++ b/packages/tonik_generate/test/src/util/to_json_value_expression_generator_test.dart
@@ -67,7 +67,7 @@ void main() {
);
expect(
buildToJsonPropertyExpression('dueDate', property),
- 'dueDate?.toIso8601String()',
+ 'dueDate?.toJson()',
);
});
diff --git a/packages/tonik_parse/CHANGELOG.md b/packages/tonik_parse/CHANGELOG.md
index ca6a9fc..85629aa 100644
--- a/packages/tonik_parse/CHANGELOG.md
+++ b/packages/tonik_parse/CHANGELOG.md
@@ -1,3 +1,5 @@
+## 0.0.6
+
## 0.0.5
## 0.0.4
diff --git a/packages/tonik_parse/pubspec.yaml b/packages/tonik_parse/pubspec.yaml
index 6f1e610..0dfe1a0 100644
--- a/packages/tonik_parse/pubspec.yaml
+++ b/packages/tonik_parse/pubspec.yaml
@@ -1,6 +1,6 @@
name: tonik_parse
description: The parsing module for Tonik.
-version: 0.0.5
+version: 0.0.6
repository: https://github.com/t-unit/tonik
resolution: workspace
@@ -11,10 +11,9 @@ dependencies:
collection: ^1.19.1
json_annotation: ^4.9.0
logging: ^1.3.0
- tonik_core: ^0.0.5
+ tonik_core: ^0.0.6
dev_dependencies:
build_runner: ^2.3.3
json_serializable: ^6.8.0
test: ^1.24.0
- very_good_analysis: ^8.0.0
diff --git a/packages/tonik_util/CHANGELOG.md b/packages/tonik_util/CHANGELOG.md
index b0c37af..8a18a94 100644
--- a/packages/tonik_util/CHANGELOG.md
+++ b/packages/tonik_util/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.0.6
+
+ - **FIX**: proper handle dates.
+
## 0.0.5
## 0.0.4
diff --git a/packages/tonik_util/lib/src/date.dart b/packages/tonik_util/lib/src/date.dart
index 0f94661..b422e3f 100644
--- a/packages/tonik_util/lib/src/date.dart
+++ b/packages/tonik_util/lib/src/date.dart
@@ -10,7 +10,7 @@ import 'package:tonik_util/src/decoding/simple_decoder.dart';
class Date {
/// Creates a new [Date] instance.
///
- /// Throws [RangeError] if any of the date components are invalid.
+ /// Throws [FormatException] if any of the date components are invalid.
Date(this.year, this.month, this.day) {
_validate();
}
@@ -24,7 +24,8 @@ class Date {
/// Creates a [Date] from an ISO 8601 formatted string (YYYY-MM-DD).
///
- /// Throws [FormatException] if the string is not in the correct format.
+ /// Throws [FormatException] if the string is not in the correct format
+ /// or if any of the date components are invalid.
factory Date.fromString(String dateString) {
final parts = dateString.split('-');
if (parts.length != 3) {
@@ -37,7 +38,7 @@ class Date {
final day = int.parse(parts[2]);
final date = Date(year, month, day).._validate();
return date;
- } on FormatException {
+ } on Object {
throw const FormatException('Invalid date format. Expected YYYY-MM-DD');
}
}
@@ -114,12 +115,17 @@ class Date {
void _validate() {
if (month < 1 || month > 12) {
- throw RangeError.range(month, 1, 12, 'month');
+ throw FormatException(
+ 'Invalid month: $month. Month must be between 1 and 12.',
+ );
}
final daysInMonth = DateTime(year, month + 1, 0).day;
if (day < 1 || day > daysInMonth) {
- throw RangeError.range(day, 1, daysInMonth, 'day');
+ throw FormatException(
+ 'Invalid day: $day. Day must be between 1 and $daysInMonth for '
+ 'month $month.',
+ );
}
}
}
diff --git a/packages/tonik_util/lib/src/decoding/json_decoder.dart b/packages/tonik_util/lib/src/decoding/json_decoder.dart
index 4192acf..957f61d 100644
--- a/packages/tonik_util/lib/src/decoding/json_decoder.dart
+++ b/packages/tonik_util/lib/src/decoding/json_decoder.dart
@@ -1,4 +1,5 @@
import 'package:big_decimal/big_decimal.dart';
+import 'package:tonik_util/src/date.dart';
import 'package:tonik_util/src/decoding/decoding_exception.dart';
/// Extensions for decoding JSON values.
@@ -348,4 +349,46 @@ extension JsonDecoder on Object? {
}
return this! as bool;
}
+
+ /// Decodes a JSON value to a Date.
+ ///
+ /// Expects ISO 8601 format string (YYYY-MM-DD).
+ /// Throws [InvalidTypeException] if the value is not a valid date string
+ /// or if the value is null.
+ Date decodeJsonDate({String? context}) {
+ if (this == null) {
+ throw InvalidTypeException(
+ value: 'null',
+ targetType: Date,
+ context: context,
+ );
+ }
+ if (this is! String) {
+ throw InvalidTypeException(
+ value: toString(),
+ targetType: Date,
+ context: context,
+ );
+ }
+ try {
+ return Date.fromString(this! as String);
+ } on FormatException catch (e) {
+ throw InvalidTypeException(
+ value: this! as String,
+ targetType: Date,
+ context: e.message,
+ );
+ }
+ }
+
+ /// Decodes a JSON value to a nullable Date.
+ ///
+ /// Returns null if the value is null or an empty string.
+ /// Throws [InvalidTypeException] if the value is not a valid date string.
+ Date? decodeJsonNullableDate({String? context}) {
+ if (this == null || (this is String && (this! as String).isEmpty)) {
+ return null;
+ }
+ return decodeJsonDate(context: context);
+ }
}
diff --git a/packages/tonik_util/lib/src/decoding/simple_decoder.dart b/packages/tonik_util/lib/src/decoding/simple_decoder.dart
index c5f7957..9f7d9a8 100644
--- a/packages/tonik_util/lib/src/decoding/simple_decoder.dart
+++ b/packages/tonik_util/lib/src/decoding/simple_decoder.dart
@@ -1,4 +1,5 @@
import 'package:big_decimal/big_decimal.dart';
+import 'package:tonik_util/src/date.dart';
import 'package:tonik_util/src/decoding/decoding_exception.dart';
/// Extensions for decoding simple form values from strings.
@@ -253,4 +254,49 @@ extension SimpleDecoder on String? {
if (this?.isEmpty ?? true) return null;
return decodeSimpleStringNullableList(context: context);
}
+
+ /// Decodes a string to a Date.
+ ///
+ /// The string must be in ISO 8601 format (YYYY-MM-DD).
+ /// Throws [FormatException] if the string is not in the correct format or if
+ /// any of the date components are invalid.
+ /// Throws [InvalidTypeException] if the value is null or empty.
+ Date decodeSimpleDate({String? context}) {
+ if (this == null) {
+ throw InvalidTypeException(
+ value: 'null',
+ targetType: Date,
+ context: context,
+ );
+ }
+ if (this!.isEmpty) {
+ throw InvalidTypeException(
+ value: 'empty string',
+ targetType: Date,
+ context: context,
+ );
+ }
+ try {
+ return Date.fromString(this!);
+ } on FormatException {
+ rethrow;
+ } on Object {
+ throw InvalidTypeException(
+ value: this!,
+ targetType: Date,
+ context: context,
+ );
+ }
+ }
+
+ /// Decodes a string to a nullable Date.
+ ///
+ /// Returns null if the string is empty or null.
+ /// The string must be in ISO 8601 format (YYYY-MM-DD).
+ /// Throws [FormatException] if the string is not in the correct format or if
+ /// any of the date components are invalid.
+ Date? decodeSimpleNullableDate({String? context}) {
+ if (this?.isEmpty ?? true) return null;
+ return decodeSimpleDate(context: context);
+ }
}
diff --git a/packages/tonik_util/lib/src/dio/server_config.dart b/packages/tonik_util/lib/src/dio/server_config.dart
index f5fa6f4..8f4c48e 100644
--- a/packages/tonik_util/lib/src/dio/server_config.dart
+++ b/packages/tonik_util/lib/src/dio/server_config.dart
@@ -50,15 +50,15 @@ class ServerConfig {
// Set the server URL
dio.options.baseUrl = serverUrl;
-
+
// Add all interceptors
for (final interceptor in interceptors) {
dio.interceptors.add(interceptor);
}
-
+
// Set httpClientAdapter if provided
if (httpClientAdapter != null) {
dio.httpClientAdapter = httpClientAdapter!;
}
}
-}
+}
diff --git a/packages/tonik_util/pubspec.yaml b/packages/tonik_util/pubspec.yaml
index 00b6106..2bccc0a 100644
--- a/packages/tonik_util/pubspec.yaml
+++ b/packages/tonik_util/pubspec.yaml
@@ -1,6 +1,6 @@
name: tonik_util
description: Runtime tools for packages generated by Tonik.
-version: 0.0.5
+version: 0.0.6
repository: https://github.com/t-unit/tonik
resolution: workspace
@@ -15,4 +15,3 @@ dependencies:
dev_dependencies:
test: ^1.24.0
- very_good_analysis: ^8.0.0
diff --git a/packages/tonik_util/test/src/date_test.dart b/packages/tonik_util/test/src/date_test.dart
index 98c3be7..25ce1b3 100644
--- a/packages/tonik_util/test/src/date_test.dart
+++ b/packages/tonik_util/test/src/date_test.dart
@@ -98,13 +98,25 @@ void main() {
});
test('fromJson throws on invalid date values', () {
- expect(() => Date.fromJson('2024-00-15'), throwsA(isA()));
- expect(() => Date.fromJson('2024-13-15'), throwsA(isA()));
- expect(() => Date.fromJson('2024-03-00'), throwsA(isA()));
- expect(() => Date.fromJson('2024-03-32'), throwsA(isA()));
+ expect(
+ () => Date.fromJson('2024-00-15'),
+ throwsA(isA()),
+ );
+ expect(
+ () => Date.fromJson('2024-13-15'),
+ throwsA(isA()),
+ );
+ expect(
+ () => Date.fromJson('2024-03-00'),
+ throwsA(isA()),
+ );
+ expect(
+ () => Date.fromJson('2024-03-32'),
+ throwsA(isA()),
+ );
expect(
() => Date.fromJson('2024-02-30'),
- throwsA(isA()),
+ throwsA(isA()),
); // February 30th
});
@@ -128,24 +140,36 @@ void main() {
});
test('fromSimple throws on invalid date values', () {
- expect(() => Date.fromSimple('2024-00-15'), throwsA(isA()));
- expect(() => Date.fromSimple('2024-13-15'), throwsA(isA()));
- expect(() => Date.fromSimple('2024-03-00'), throwsA(isA()));
- expect(() => Date.fromSimple('2024-03-32'), throwsA(isA()));
+ expect(
+ () => Date.fromSimple('2024-00-15'),
+ throwsA(isA()),
+ );
+ expect(
+ () => Date.fromSimple('2024-13-15'),
+ throwsA(isA()),
+ );
+ expect(
+ () => Date.fromSimple('2024-03-00'),
+ throwsA(isA()),
+ );
+ expect(
+ () => Date.fromSimple('2024-03-32'),
+ throwsA(isA()),
+ );
expect(
() => Date.fromSimple('2024-02-30'),
- throwsA(isA()),
+ throwsA(isA()),
); // February 30th
});
test('validates date components', () {
- expect(() => Date(2024, 0, 15), throwsA(isA()));
- expect(() => Date(2024, 13, 15), throwsA(isA()));
- expect(() => Date(2024, 3, 0), throwsA(isA()));
- expect(() => Date(2024, 3, 32), throwsA(isA()));
+ expect(() => Date(2024, 0, 15), throwsA(isA()));
+ expect(() => Date(2024, 13, 15), throwsA(isA()));
+ expect(() => Date(2024, 3, 0), throwsA(isA()));
+ expect(() => Date(2024, 3, 32), throwsA(isA()));
expect(
() => Date(2024, 2, 30),
- throwsA(isA()),
+ throwsA(isA()),
); // February 30th
});
});
diff --git a/packages/tonik_util/test/src/decoding/json_decoder_test.dart b/packages/tonik_util/test/src/decoding/json_decoder_test.dart
index f2cc630..1b21dc2 100644
--- a/packages/tonik_util/test/src/decoding/json_decoder_test.dart
+++ b/packages/tonik_util/test/src/decoding/json_decoder_test.dart
@@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:big_decimal/big_decimal.dart';
import 'package:test/test.dart';
+import 'package:tonik_util/src/date.dart';
import 'package:tonik_util/src/decoding/decoding_exception.dart';
import 'package:tonik_util/src/decoding/json_decoder.dart';
@@ -196,6 +197,72 @@ void main() {
);
});
});
+
+ group('Date', () {
+ test('decodes Date values', () {
+ final date = Date(2024, 3, 15);
+ expect('2024-03-15'.decodeJsonDate(), date);
+ expect(
+ () => 123.decodeJsonDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => null.decodeJsonDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => '2024-00-15'.decodeJsonDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => '2024-13-15'.decodeJsonDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => '2024-03-00'.decodeJsonDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => '2024-03-32'.decodeJsonDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => '2024-02-30'.decodeJsonDate(),
+ throwsA(isA()),
+ );
+ });
+
+ test('decodes nullable Date values', () {
+ final date = Date(2024, 3, 15);
+ expect('2024-03-15'.decodeJsonNullableDate(), date);
+ expect(null.decodeJsonNullableDate(), isNull);
+ expect(''.decodeJsonNullableDate(), isNull);
+ expect(
+ () => 123.decodeJsonNullableDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => '2024-00-15'.decodeJsonNullableDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => '2024-13-15'.decodeJsonNullableDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => '2024-03-00'.decodeJsonNullableDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => '2024-03-32'.decodeJsonNullableDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => '2024-02-30'.decodeJsonNullableDate(),
+ throwsA(isA()),
+ );
+ });
+ });
});
group('List', () {
diff --git a/packages/tonik_util/test/src/decoding/simple_decoder_test.dart b/packages/tonik_util/test/src/decoding/simple_decoder_test.dart
index 85818cc..366b6a1 100644
--- a/packages/tonik_util/test/src/decoding/simple_decoder_test.dart
+++ b/packages/tonik_util/test/src/decoding/simple_decoder_test.dart
@@ -1,5 +1,6 @@
import 'package:big_decimal/big_decimal.dart';
import 'package:test/test.dart';
+import 'package:tonik_util/src/date.dart';
import 'package:tonik_util/src/decoding/decoding_exception.dart';
import 'package:tonik_util/src/decoding/simple_decoder.dart';
@@ -70,6 +71,39 @@ void main() {
throwsA(isA()),
);
});
+
+ test('decodes Date values', () {
+ final date = Date(2024, 3, 15);
+ expect('2024-03-15'.decodeSimpleDate(), date);
+ expect(
+ () => 'not-a-date'.decodeSimpleDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => null.decodeSimpleDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => '2024-00-15'.decodeSimpleDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => '2024-13-15'.decodeSimpleDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => '2024-03-00'.decodeSimpleDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => '2024-03-32'.decodeSimpleDate(),
+ throwsA(isA()),
+ );
+ expect(
+ () => '2024-02-30'.decodeSimpleDate(),
+ throwsA(isA()),
+ );
+ });
});
group('Nullable Values', () {
@@ -79,12 +113,14 @@ void main() {
expect(''.decodeSimpleNullableBool(), isNull);
expect(''.decodeSimpleNullableDateTime(), isNull);
expect(''.decodeSimpleNullableBigDecimal(), isNull);
+ expect(''.decodeSimpleNullableDate(), isNull);
expect(null.decodeSimpleNullableInt(), isNull);
expect(null.decodeSimpleNullableDouble(), isNull);
expect(null.decodeSimpleNullableBool(), isNull);
expect(null.decodeSimpleNullableDateTime(), isNull);
expect(null.decodeSimpleNullableBigDecimal(), isNull);
+ expect(null.decodeSimpleNullableDate(), isNull);
});
test('decodes non-empty strings for nullable types', () {
@@ -99,6 +135,10 @@ void main() {
'3.14'.decodeSimpleNullableBigDecimal(),
BigDecimal.parse('3.14'),
);
+ expect(
+ '2024-03-15'.decodeSimpleNullableDate(),
+ Date(2024, 3, 15),
+ );
});
});
@@ -124,6 +164,10 @@ void main() {
() => ''.decodeSimpleBigDecimal(),
throwsA(isA()),
);
+ expect(
+ () => ''.decodeSimpleDate(),
+ throwsA(isA()),
+ );
});
});
diff --git a/packages/tonik_util/test/src/encoder/deep_object_encoder_test.dart b/packages/tonik_util/test/src/encoder/deep_object_encoder_test.dart
index 1182669..9bfd98b 100644
--- a/packages/tonik_util/test/src/encoder/deep_object_encoder_test.dart
+++ b/packages/tonik_util/test/src/encoder/deep_object_encoder_test.dart
@@ -7,10 +7,14 @@ void main() {
group('DeepObjectEncoder', () {
test('encodes a simple object', () {
- final result = encoder.encode('filter', {
- 'color': 'red',
- 'size': 'large',
- }, allowEmpty: true,);
+ final result = encoder.encode(
+ 'filter',
+ {
+ 'color': 'red',
+ 'size': 'large',
+ },
+ allowEmpty: true,
+ );
expect(result, [
(name: 'filter[color]', value: 'red'),
@@ -19,10 +23,14 @@ void main() {
});
test('encodes boolean properties', () {
- final result = encoder.encode('filter', {
- 'active': true,
- 'premium': false,
- }, allowEmpty: true,);
+ final result = encoder.encode(
+ 'filter',
+ {
+ 'active': true,
+ 'premium': false,
+ },
+ allowEmpty: true,
+ );
expect(result, [
(name: 'filter[active]', value: 'true'),
@@ -31,10 +39,14 @@ void main() {
});
test('encodes an object with a null value', () {
- final result = encoder.encode('filter', {
- 'color': null,
- 'size': 'large',
- }, allowEmpty: true,);
+ final result = encoder.encode(
+ 'filter',
+ {
+ 'color': null,
+ 'size': 'large',
+ },
+ allowEmpty: true,
+ );
expect(result, [
(name: 'filter[color]', value: ''),
@@ -53,9 +65,13 @@ void main() {
});
test('encodes nested objects', () {
- final result = encoder.encode('filter', {
- 'product': {'color': 'blue', 'size': 'medium'},
- }, allowEmpty: true,);
+ final result = encoder.encode(
+ 'filter',
+ {
+ 'product': {'color': 'blue', 'size': 'medium'},
+ },
+ allowEmpty: true,
+ );
expect(result, [
(name: 'filter[product][color]', value: 'blue'),
@@ -64,11 +80,15 @@ void main() {
});
test('encodes deeply nested objects', () {
- final result = encoder.encode('filter', {
- 'product': {
- 'attributes': {'color': 'blue', 'size': 'medium'},
+ final result = encoder.encode(
+ 'filter',
+ {
+ 'product': {
+ 'attributes': {'color': 'blue', 'size': 'medium'},
+ },
},
- }, allowEmpty: true,);
+ allowEmpty: true,
+ );
expect(result, [
(name: 'filter[product][attributes][color]', value: 'blue'),
@@ -78,9 +98,13 @@ void main() {
test('throws for objects containing arrays', () {
expect(
- () => encoder.encode('filter', {
- 'colors': ['red', 'blue', 'green'],
- }, allowEmpty: true,),
+ () => encoder.encode(
+ 'filter',
+ {
+ 'colors': ['red', 'blue', 'green'],
+ },
+ allowEmpty: true,
+ ),
throwsA(isA()),
);
});
@@ -95,20 +119,28 @@ void main() {
test('throws for objects containing sets', () {
expect(
- () => encoder.encode('filter', {
- 'sizes': {'small', 'medium', 'large'},
- }, allowEmpty: true,),
+ () => encoder.encode(
+ 'filter',
+ {
+ 'sizes': {'small', 'medium', 'large'},
+ },
+ allowEmpty: true,
+ ),
throwsA(isA()),
);
});
test('encodes a complex object with various types', () {
- final result = encoder.encode('params', {
- 'name': 'John',
- 'age': 30,
- 'active': true,
- 'address': {'street': '123 Main St', 'city': 'New York'},
- }, allowEmpty: true,);
+ final result = encoder.encode(
+ 'params',
+ {
+ 'name': 'John',
+ 'age': 30,
+ 'active': true,
+ 'address': {'street': '123 Main St', 'city': 'New York'},
+ },
+ allowEmpty: true,
+ );
expect(result, [
(name: 'params[name]', value: 'John'),
@@ -180,11 +212,15 @@ void main() {
group('allowEmpty parameter', () {
test('allows empty values when allowEmpty is true', () {
- final result = encoder.encode('filter', {
- 'emptyString': '',
- 'emptyMap': {},
- 'normalValue': 'test',
- }, allowEmpty: true,);
+ final result = encoder.encode(
+ 'filter',
+ {
+ 'emptyString': '',
+ 'emptyMap': {},
+ 'normalValue': 'test',
+ },
+ allowEmpty: true,
+ );
expect(result, [
(name: 'filter[emptyString]', value: ''),
@@ -195,10 +231,14 @@ void main() {
test('throws when allowEmpty is false and value is empty string', () {
expect(
- () => encoder.encode('filter', {
- 'emptyString': '',
- 'normalValue': 'test',
- }, allowEmpty: false,),
+ () => encoder.encode(
+ 'filter',
+ {
+ 'emptyString': '',
+ 'normalValue': 'test',
+ },
+ allowEmpty: false,
+ ),
throwsA(isA()),
);
});
@@ -213,10 +253,14 @@ void main() {
test('throws when allowEmpty is false and nested map is empty', () {
expect(
- () => encoder.encode('filter', {
- 'nested': {},
- 'normalValue': 'test',
- }, allowEmpty: false,),
+ () => encoder.encode(
+ 'filter',
+ {
+ 'nested': {},
+ 'normalValue': 'test',
+ },
+ allowEmpty: false,
+ ),
throwsA(isA()),
);
});
@@ -238,10 +282,14 @@ void main() {
});
test('allows non-empty values when allowEmpty is false', () {
- final result = encoder.encode('filter', {
- 'string': 'value',
- 'nested': {'inner': 'value'},
- }, allowEmpty: false,);
+ final result = encoder.encode(
+ 'filter',
+ {
+ 'string': 'value',
+ 'nested': {'inner': 'value'},
+ },
+ allowEmpty: false,
+ );
expect(result, [
(name: 'filter[string]', value: 'value'),
diff --git a/pubspec.yaml b/pubspec.yaml
index 403f4d7..9d59be4 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -26,7 +26,7 @@ dependencies:
dev_dependencies:
melos: ^7.0.0-dev.9
test: ^1.25.15
- very_good_analysis: ^8.0.0
+ very_good_analysis: ^9.0.0
melos:
scripts: