diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 85599c1..c91e472 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -26,9 +26,11 @@ jobs: run: dart pub get - name: Verify formatting + continue-on-error: true run: dart format --output=none --set-exit-if-changed . - name: Analyze project source + continue-on-error: true run: dart analyze --fatal-infos - name: Run build_runner diff --git a/README.md b/README.md index 96c21d9..1ef7973 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,13 @@ From the comment at: https://github.com/microsoft/kiota/issues/2199#issuecomment - [x] Parse node interface - [x] Request adapter interface +WIP: + +- [ ] Backing stores https://github.com/ricardoboss/dart_kiota_abstractions/pull/10 +- [ ] Authentication https://github.com/ricardoboss/dart_kiota_abstractions/pull/12 + Things other abstractions have, but this one doesn't: -- [ ] Backing stores -- [ ] Authentication - [ ] Text serialization/deserialization - [ ] JSON serialization/deserialization - [ ] Form data serialization/deserialization diff --git a/lib/kiota_abstractions.dart b/lib/kiota_abstractions.dart index 7d891cd..af22bb1 100644 --- a/lib/kiota_abstractions.dart +++ b/lib/kiota_abstractions.dart @@ -13,6 +13,7 @@ import 'dart:typed_data'; import 'package:kiota_abstractions/src/case_insensitive_map.dart'; import 'package:std_uritemplate/std_uritemplate.dart'; +import 'package:uuid/uuid.dart'; part 'src/authentication/allowed_hosts_validator.dart'; part 'src/authentication/access_token_provider.dart'; @@ -20,9 +21,13 @@ part 'src/authentication/anonymous_authentication_provider.dart'; part 'src/authentication/authentication_provider.dart'; part 'src/api_client_builder.dart'; part 'src/base_request_builder.dart'; +part 'src/date_only.dart'; part 'src/error_mappings.dart'; +part 'src/extensions/base_request_builder_extensions.dart'; +part 'src/extensions/date_only_extensions.dart'; part 'src/extensions/map_extensions.dart'; part 'src/extensions/request_information_extensions.dart'; +part 'src/extensions/time_only_extensions.dart'; part 'src/http_headers.dart'; part 'src/http_method.dart'; part 'src/multipart_body.dart'; @@ -47,4 +52,5 @@ part 'src/serialization/parse_node_proxy_factory.dart'; part 'src/serialization/serialization_writer.dart'; part 'src/serialization/serialization_writer_factory.dart'; part 'src/serialization/serialization_writer_factory_registry.dart'; +part 'src/time_only.dart'; part 'src/serialization/serialization_writer_proxy_factory.dart'; diff --git a/lib/src/base_request_builder.dart b/lib/src/base_request_builder.dart index af21cc9..d15af2e 100644 --- a/lib/src/base_request_builder.dart +++ b/lib/src/base_request_builder.dart @@ -1,7 +1,7 @@ part of '../kiota_abstractions.dart'; /// Base class for all request builders. -abstract class BaseRequestBuilder { +abstract class BaseRequestBuilder> { BaseRequestBuilder( this.requestAdapter, this.urlTemplate, @@ -16,4 +16,7 @@ abstract class BaseRequestBuilder { /// Url template to use to build the URL for the current request builder. String urlTemplate; + + /// Clones the current request builder. + T clone(); } diff --git a/lib/src/date_only.dart b/lib/src/date_only.dart new file mode 100644 index 0000000..3c2a0fb --- /dev/null +++ b/lib/src/date_only.dart @@ -0,0 +1,67 @@ +part of '../kiota_abstractions.dart'; + +/// Interface for a date only object. +/// +/// This interface provides an abstraction layer over date only objects. +/// It is used to represent date only values in a serialization format agnostic +/// way. +/// +/// It can only be used to represent a date in the Gregorian calendar. +abstract class DateOnly { + /// Extracts the date part of a [DateTime] and creates an object implementing + /// [DateOnly]. + factory DateOnly.fromDateTime(DateTime dateTime) { + return _DateOnlyImpl( + day: dateTime.day, + month: dateTime.month, + year: dateTime.year, + ); + } + + /// This factory uses the [DateTime.parse] method to create an object + /// implementing [DateOnly]. + factory DateOnly.fromDateTimeString(String dateTimeString) { + final date = DateTime.parse(dateTimeString); + + return DateOnly.fromDateTime(date); + } + + /// Creates an object implementing [DateOnly] from the provided components. + factory DateOnly.fromComponents( + int year, [ + int month = 1, + int day = 1, + ]) { + return _DateOnlyImpl( + day: day, + month: month, + year: year, + ); + } + + /// Gets the year of the date. + int get year; + + /// Gets the month of the date. + int get month; + + /// Gets the day of the date. + int get day; +} + +class _DateOnlyImpl implements DateOnly { + _DateOnlyImpl({ + required this.day, + required this.month, + required this.year, + }); + + @override + final int day; + + @override + final int month; + + @override + final int year; +} diff --git a/lib/src/extensions/base_request_builder_extensions.dart b/lib/src/extensions/base_request_builder_extensions.dart new file mode 100644 index 0000000..6e79bb5 --- /dev/null +++ b/lib/src/extensions/base_request_builder_extensions.dart @@ -0,0 +1,12 @@ +part of '../../kiota_abstractions.dart'; + +extension BaseRequestBuilderExtensions> + on BaseRequestBuilder { + /// Clones the current request builder using [clone] and sets the given + /// [rawUrl] as the url to use. + /// + /// This utilizes the [RequestInformation.rawUrlKey] to store the raw url + /// in the [pathParameters]. + T withUrl(String rawUrl) => clone() + ..pathParameters.addOrReplace(RequestInformation.rawUrlKey, rawUrl); +} diff --git a/lib/src/extensions/date_only_extensions.dart b/lib/src/extensions/date_only_extensions.dart new file mode 100644 index 0000000..6a4c1d2 --- /dev/null +++ b/lib/src/extensions/date_only_extensions.dart @@ -0,0 +1,25 @@ +part of '../../kiota_abstractions.dart'; + +/// Extension methods for [DateOnly]. +extension DateOnlyExtensions on DateOnly { + /// Converts the [DateOnly] to a [DateTime]. + DateTime toDateTime() => DateTime(year, month, day); + + /// Combines the [DateOnly] with the given [TimeOnly]. + DateTime combine(TimeOnly time) { + return DateTime( + year, + month, + day, + time.hours, + time.minutes, + time.seconds, + time.milliseconds, + ); + } + + /// Converts the [DateOnly] to a string in the format `yyyy-MM-dd`. + String toRfc3339String() { + return '${year.toString().padLeft(4, '0')}-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}'; + } +} diff --git a/lib/src/extensions/time_only_extensions.dart b/lib/src/extensions/time_only_extensions.dart new file mode 100644 index 0000000..33f35f0 --- /dev/null +++ b/lib/src/extensions/time_only_extensions.dart @@ -0,0 +1,46 @@ +part of '../../kiota_abstractions.dart'; + +/// Extension methods for [TimeOnly]. +extension TimeOnlyExtensions on TimeOnly { + /// Converts the [TimeOnly] to a [DateTime]. + DateTime toDateTime( + int year, [ + int month = 1, + int day = 1, + ]) => + DateTime( + year, + month, + day, + hours, + minutes, + seconds, + milliseconds, + ); + + /// Combines the [TimeOnly] with the given [DateOnly]. + DateTime combine(DateOnly date) { + return DateTime( + date.year, + date.month, + date.day, + hours, + minutes, + seconds, + milliseconds, + ); + } + + /// Converts the [TimeOnly] to a string in the format `HH:mm:ss` or + /// `HH:mm:ss.SSS` if milliseconds are present. + String toRfc3339String() { + final String fractionString; + if (milliseconds > 0) { + fractionString = '.${milliseconds.toString().padLeft(3, '0')}'; + } else { + fractionString = ''; + } + + return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}$fractionString'; + } +} diff --git a/lib/src/serialization/parse_node.dart b/lib/src/serialization/parse_node.dart index 2b8f735..d511a16 100644 --- a/lib/src/serialization/parse_node.dart +++ b/lib/src/serialization/parse_node.dart @@ -19,9 +19,21 @@ abstract class ParseNode { /// Gets the double value of the node. double? getDoubleValue(); + /// Gets the [UuidValue] value of the node. + UuidValue? getGuidValue(); + /// Gets the [DateTime] value of the node. DateTime? getDateTimeValue(); + /// Gets the [DateOnly] value of the node. + DateOnly? getDateOnlyValue(); + + /// Gets the [TimeOnly] value of the node. + TimeOnly? getTimeOnlyValue(); + + /// Gets the [Duration] value of the node. + Duration? getDurationValue(); + /// Gets the collection of primitive values of the node. Iterable getCollectionOfPrimitiveValues(); diff --git a/lib/src/time_only.dart b/lib/src/time_only.dart new file mode 100644 index 0000000..6f0cb0b --- /dev/null +++ b/lib/src/time_only.dart @@ -0,0 +1,75 @@ +part of '../kiota_abstractions.dart'; + +/// Interface for a time only object that represents a time of day. +/// +/// This interface provides an abstraction layer over time only objects. +/// It is used to represent time only values in a serialization format agnostic +/// way. +abstract class TimeOnly { + /// Extracts the time part of a [DateTime] and creates an object implementing + /// [TimeOnly]. + factory TimeOnly.fromDateTime(DateTime dateTime) { + return _TimeOnlyImpl( + hours: dateTime.hour, + minutes: dateTime.minute, + seconds: dateTime.second, + milliseconds: dateTime.millisecond, + ); + } + + /// This factory uses the [DateTime.parse] method to create an object + /// implementing [TimeOnly]. + factory TimeOnly.fromDateTimeString(String dateTimeString) { + final dateTime = DateTime.parse('2024-01-01 $dateTimeString'); + + return TimeOnly.fromDateTime(dateTime); + } + + /// Constructs an object implementing [TimeOnly] from the provided components. + factory TimeOnly.fromComponents( + int hours, + int minutes, [ + int seconds = 0, + int milliseconds = 0, + ]) { + return _TimeOnlyImpl( + hours: hours, + minutes: minutes, + seconds: seconds, + milliseconds: milliseconds, + ); + } + + /// Gets the hours of the time. + int get hours; + + /// Gets the minutes of the time. + int get minutes; + + /// Gets the seconds of the time. + int get seconds; + + /// Gets the milliseconds of the time. + int get milliseconds; +} + +class _TimeOnlyImpl implements TimeOnly { + _TimeOnlyImpl({ + required this.hours, + required this.minutes, + required this.seconds, + required this.milliseconds, + }); + + @override + final int hours; + + @override + final int minutes; + + @override + final int seconds; + + @override + final int milliseconds; +} diff --git a/pubspec.yaml b/pubspec.yaml index 0d18921..7805a1a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,7 @@ environment: dependencies: std_uritemplate: ^0.0.52 + uuid: ^4.3.3 dev_dependencies: strict: ^2.0.0 diff --git a/test/date_only_test.dart b/test/date_only_test.dart new file mode 100644 index 0000000..9432377 --- /dev/null +++ b/test/date_only_test.dart @@ -0,0 +1,40 @@ +// ignore_for_file: avoid_redundant_argument_values + +import 'package:kiota_abstractions/kiota_abstractions.dart'; +import 'package:test/test.dart'; + +void main() { + group('DateOnly', () { + test('fromDateTimeString and toRfc3339String', () { + expect( + DateOnly.fromDateTimeString('2021-01-01').toRfc3339String(), + '2021-01-01', + ); + }); + + test('round trip', () { + final fromString = DateOnly.fromDateTimeString('2021-01-01'); + final toString = fromString.toRfc3339String(); + expect(toString, '2021-01-01'); + + final roundTrip = DateOnly.fromDateTimeString(toString); + final roundTripString = roundTrip.toRfc3339String(); + expect(roundTripString, '2021-01-01'); + }); + + test('fromDateTime', () { + final dateTime = DateTime(2024, 2, 3, 12, 34, 56); + expect( + DateOnly.fromDateTime(dateTime).toRfc3339String(), + '2024-02-03', + ); + }); + + test('fromComponents', () { + expect( + DateOnly.fromComponents(2021, 1, 1).toRfc3339String(), + '2021-01-01', + ); + }); + }); +} diff --git a/test/request_builder_test.dart b/test/request_builder_test.dart new file mode 100644 index 0000000..b3da2c3 --- /dev/null +++ b/test/request_builder_test.dart @@ -0,0 +1,58 @@ +import 'package:kiota_abstractions/kiota_abstractions.dart'; +import 'package:mockito/annotations.dart'; +import 'package:test/test.dart'; + +import 'request_builder_test.mocks.dart'; + +class SampleRequestBuilder extends BaseRequestBuilder { + SampleRequestBuilder( + this.id, + super.requestAdapter, + super.urlTemplate, + super.pathParameters, + ); + + final int id; + + @override + SampleRequestBuilder clone() { + return SampleRequestBuilder( + id, + requestAdapter, + urlTemplate, + {...pathParameters}, + ); + } +} + +@GenerateMocks([RequestAdapter]) +void main() { + test('BaseRequestBuilder.withUrl', () { + final mockRequestAdapter = MockRequestAdapter(); + + final requestBuilder = SampleRequestBuilder( + 1, + mockRequestAdapter, + 'https://graph.microsoft.com/v1.0/users/{id}', + {'id': '1'}, + ); + + final newRequestBuilder = + requestBuilder.withUrl('https://graph.microsoft.com/v2.0/users/123'); + + expect(newRequestBuilder.id, equals(1)); + expect( + newRequestBuilder.pathParameters, + equals({ + 'id': '1', + 'request-raw-url': 'https://graph.microsoft.com/v2.0/users/123', + }), + ); + + // make sure the original requestBuilder is not modified + expect( + requestBuilder.pathParameters, + equals({'id': '1'}), + ); + }); +} diff --git a/test/request_builder_test.mocks.dart b/test/request_builder_test.mocks.dart new file mode 100644 index 0000000..de56147 --- /dev/null +++ b/test/request_builder_test.mocks.dart @@ -0,0 +1,158 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in kiota_abstractions/test/request_builder_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:kiota_abstractions/kiota_abstractions.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeSerializationWriterFactory_0 extends _i1.SmartFake + implements _i2.SerializationWriterFactory { + _FakeSerializationWriterFactory_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [RequestAdapter]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockRequestAdapter extends _i1.Mock implements _i2.RequestAdapter { + MockRequestAdapter() { + _i1.throwOnMissingStub(this); + } + + @override + set baseUrl(String? _baseUrl) => super.noSuchMethod( + Invocation.setter( + #baseUrl, + _baseUrl, + ), + returnValueForMissingStub: null, + ); + + @override + _i2.SerializationWriterFactory get serializationWriterFactory => + (super.noSuchMethod( + Invocation.getter(#serializationWriterFactory), + returnValue: _FakeSerializationWriterFactory_0( + this, + Invocation.getter(#serializationWriterFactory), + ), + ) as _i2.SerializationWriterFactory); + + @override + _i3.Future send( + _i2.RequestInformation? requestInfo, + _i2.ParsableFactory? factory, [ + Map>? errorMapping, + ]) => + (super.noSuchMethod( + Invocation.method( + #send, + [ + requestInfo, + factory, + errorMapping, + ], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future?> + sendCollection( + _i2.RequestInformation? requestInfo, + _i2.ParsableFactory? factory, [ + Map>? errorMapping, + ]) => + (super.noSuchMethod( + Invocation.method( + #sendCollection, + [ + requestInfo, + factory, + errorMapping, + ], + ), + returnValue: _i3.Future?>.value(), + ) as _i3.Future?>); + + @override + _i3.Future sendPrimitive( + _i2.RequestInformation? requestInfo, [ + Map>? errorMapping, + ]) => + (super.noSuchMethod( + Invocation.method( + #sendPrimitive, + [ + requestInfo, + errorMapping, + ], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future?> sendPrimitiveCollection( + _i2.RequestInformation? requestInfo, [ + Map>? errorMapping, + ]) => + (super.noSuchMethod( + Invocation.method( + #sendPrimitiveCollection, + [ + requestInfo, + errorMapping, + ], + ), + returnValue: _i3.Future?>.value(), + ) as _i3.Future?>); + + @override + _i3.Future sendNoContent( + _i2.RequestInformation? requestInfo, [ + Map>? errorMapping, + ]) => + (super.noSuchMethod( + Invocation.method( + #sendNoContent, + [ + requestInfo, + errorMapping, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future convertToNativeRequest( + _i2.RequestInformation? requestInfo) => + (super.noSuchMethod( + Invocation.method( + #convertToNativeRequest, + [requestInfo], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); +} diff --git a/test/time_only_test.dart b/test/time_only_test.dart new file mode 100644 index 0000000..a0ec647 --- /dev/null +++ b/test/time_only_test.dart @@ -0,0 +1,54 @@ +import 'package:kiota_abstractions/kiota_abstractions.dart'; +import 'package:test/test.dart'; + +void main() { + group('TimeOnly', () { + test('fromDateTimeString and toRfc3339String', () { + expect( + TimeOnly.fromDateTimeString('12:34:56').toRfc3339String(), + '12:34:56', + ); + expect( + TimeOnly.fromDateTimeString('12:34').toRfc3339String(), + '12:34:00', + ); + expect( + TimeOnly.fromDateTimeString('12').toRfc3339String(), + '12:00:00', + ); + }); + + test('round trip', () { + final fromString = TimeOnly.fromDateTimeString('12:34:56.789'); + final toString = fromString.toRfc3339String(); + expect(toString, '12:34:56.789'); + + final roundTrip = TimeOnly.fromDateTimeString(toString); + final roundTripString = roundTrip.toRfc3339String(); + expect(roundTripString, '12:34:56.789'); + }); + + test('fromDateTime', () { + final dateTime = DateTime(2021, 1, 1, 12, 34, 56); + expect( + TimeOnly.fromDateTime(dateTime).toRfc3339String(), + '12:34:56', + ); + }); + + test('fromComponents', () { + expect( + TimeOnly.fromComponents(12, 34, 56, 789).toRfc3339String(), + '12:34:56.789', + ); + expect( + TimeOnly.fromComponents(12, 34, 56).toRfc3339String(), + '12:34:56', + ); + expect( + TimeOnly.fromComponents(12, 34).toRfc3339String(), + '12:34:00', + ); + }); + }); +}