这是indexloc提供的服务,不要输入任何密码
Skip to content

Uri property encoding and decoding #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 20, 2025
Merged
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
4 changes: 3 additions & 1 deletion docs/data_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This document provides information about how Tonik is mapping data types in Open
| `string` | `date-time` | `DateTime` | `dart:core` | See [Timezone-Aware DateTime Parsing](#timezone-aware-datetime-parsing) |
| `string` | `date` | `Date` | `tonik_util` | RFC3339 date format (YYYY-MM-DD) |
| `string` | `decimal`, `currency`, `money`, `number` | `BigDecimal` | `big_decimal` | High-precision decimal numbers |
| `string` | `uri`, `url` | `Uri` | `dart:core` | URI/URL parsing and validation |
| `string` | `enum` | `enum` | Generated | Custom enum type |
| `string` | (default) | `String` | `dart:core` | Standard string type |
| `number` | `float`, `double` | `double` | `dart:core` | 64-bit floating point |
Expand Down Expand Up @@ -42,5 +43,6 @@ For strings with timezone offsets (e.g., `+05:00`), Tonik intelligently selects
1. **Prefers common locations** from the timezone package's curated list of 535+ well-known timezones
2. **Accounts for DST changes** by checking the offset at the specific timestamp
3. **Avoids deprecated locations** (e.g., `US/Eastern` → `America/New_York`)
4. **Falls back to fixed offset** locations (`Etc/GMT±N`) when no match is found
4. **Attempts fixed offset locations** (`Etc/GMT±N`) for standard hour offsets when no timezone match is found
5. **Falls back to UTC** for non-standard offsets or when `Etc/GMT±N` locations are unavailable

1 change: 0 additions & 1 deletion packages/tonik/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ For a full list of changes of each release, refer to [release notes](https://git

### Short term goals
- `allowReserved` support for query parameters
- `format: uri` mapping to Dart `Uri`
- More E2E tests
- Full decoding and encoding support for any of and one of
- Support for `x-dart-name`, `x-dart-type` and `x-dart-enums`
Expand Down
7 changes: 7 additions & 0 deletions packages/tonik_core/lib/src/model/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,13 @@ class DecimalModel extends PrimitiveModel {
String toString() => 'DecimalModel';
}

class UriModel extends PrimitiveModel {
const UriModel({required super.context});

@override
String toString() => 'UriModel';
}

@immutable
class Property {
const Property({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ void main() {
DecimalModel(context: Context.initial()).encodingShape,
EncodingShape.simple,
);
expect(
UriModel(context: Context.initial()).encodingShape,
EncodingShape.simple,
);
});

test('Enum models are simple', () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ Expression buildFromJsonValueExpression(
return refer(value)
.property(isNullable ? 'decodeJsonNullableDate' : 'decodeJsonDate')
.call([], contextParam);
case UriModel():
return refer(value)
.property(isNullable ? 'decodeJsonNullableUri' : 'decodeJsonUri')
.call([], contextParam);
case ListModel():
return _buildListFromJsonExpression(
value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ Expression buildSimpleValueExpression(
isRequired
? value.property('decodeSimpleDate').call([], contextParam)
: value.property('decodeSimpleNullableDate').call([], contextParam),
UriModel() =>
isRequired
? value.property('decodeSimpleUri').call([], contextParam)
: value.property('decodeSimpleNullableUri').call([], contextParam),
EnumModel() ||
ClassModel() ||
AllOfModel() ||
Expand Down Expand Up @@ -207,6 +211,12 @@ Expression _buildListFromSimpleExpression(
isRequired,
contextParam: contextParam,
),
UriModel() => _buildPrimitiveList(
listDecode,
'decodeSimpleUri',
isRequired,
contextParam: contextParam,
),
ClassModel() =>
throw UnimplementedError(
'ClassModel is not supported in lists for simple encoding',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ String? _getSerializationSuffix(Model model, bool isNullable) {
return switch (model) {
DateTimeModel() => '$nullablePart.toTimeZonedIso8601String()',
DecimalModel() => '$nullablePart.toString()',
UriModel() => '$nullablePart.toString()',
DateModel() ||
EnumModel() ||
ClassModel() ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ TypeReference typeReference(
..url = 'package:big_decimal/big_decimal.dart'
..isNullable = isNullableOverride,
),
UriModel _ => TypeReference(
(b) =>
b
..symbol = 'Uri'
..url = 'dart:core'
..isNullable = isNullableOverride,
),
final CompositeModel m => TypeReference(
(b) =>
b
Expand Down
130 changes: 130 additions & 0 deletions packages/tonik_generate/test/src/model/class_generator_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -372,5 +372,135 @@ void main() {
expect(field.annotations, isEmpty);
});
});

test(
'generates constructor with required fields before non-required fields',
() {
final model = ClassModel(
name: 'User',
properties: [
Property(
name: 'id',
model: IntegerModel(context: context),
isRequired: true,
isNullable: false,
isDeprecated: false,
),
Property(
name: 'name',
model: StringModel(context: context),
isRequired: false,
isNullable: true,
isDeprecated: false,
),
],
context: context,
);

final result = generator.generateClass(model);
final constructor = result.constructors.first;

expect(constructor.optionalParameters, hasLength(2));

final idParam = constructor.optionalParameters[0];
expect(idParam.name, 'id');
expect(idParam.required, isTrue);

final nameParam = constructor.optionalParameters[1];
expect(nameParam.name, 'name');
expect(nameParam.required, isFalse);
},
);

test('generates field with Uri type for UriModel property', () {
final model = ClassModel(
name: 'Resource',
properties: [
Property(
name: 'endpoint',
model: UriModel(context: context),
isRequired: true,
isNullable: false,
isDeprecated: false,
),
],
context: context,
);

final result = generator.generateClass(model);
final field = result.fields.first;

expect(field.name, 'endpoint');
expect(field.modifier, FieldModifier.final$);

final typeRef = field.type! as TypeReference;
expect(typeRef.symbol, 'Uri');
expect(typeRef.url, 'dart:core');
expect(typeRef.isNullable, isFalse);
});

test('generates nullable Uri field for nullable UriModel property', () {
final model = ClassModel(
name: 'Resource',
properties: [
Property(
name: 'optionalEndpoint',
model: UriModel(context: context),
isRequired: false,
isNullable: true,
isDeprecated: false,
),
],
context: context,
);

final result = generator.generateClass(model);
final field = result.fields.first;

expect(field.name, 'optionalEndpoint');

final typeRef = field.type! as TypeReference;
expect(typeRef.symbol, 'Uri');
expect(typeRef.url, 'dart:core');
expect(typeRef.isNullable, isTrue);
});

test('generates constructor parameter for Uri property', () {
final model = ClassModel(
name: 'Resource',
properties: [
Property(
name: 'endpoint',
model: UriModel(context: context),
isRequired: true,
isNullable: false,
isDeprecated: false,
),
Property(
name: 'callback',
model: UriModel(context: context),
isRequired: false,
isNullable: true,
isDeprecated: false,
),
],
context: context,
);

final result = generator.generateClass(model);
final constructor = result.constructors.first;

expect(constructor.optionalParameters, hasLength(2));

final endpointParam = constructor.optionalParameters[0];
expect(endpointParam.name, 'endpoint');
expect(endpointParam.required, isTrue);
expect(endpointParam.toThis, isTrue);

final callbackParam = constructor.optionalParameters[1];
expect(callbackParam.name, 'callback');
expect(callbackParam.required, isFalse);
expect(callbackParam.toThis, isTrue);
});
});
}
Loading
Loading