Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
36 changes: 36 additions & 0 deletions lib/src/semantic/calendar_enums.dart
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,42 @@ extension TodoStatusExtensions on TodoStatus {
};
}

/// Trigger relation types.
enum TriggerRelated { start, end }

/// Names for [TriggerRelated] enum values
class TriggerRelatedNames {
/// The private constructor to prevent instantiation.
TriggerRelatedNames._();

/// The name for trigger relation to START.
static const start = 'START';

/// The name for trigger relation to END.
static const end = 'END';

/// Tries to parse the given string into a TriggerRelated enum value.
static TriggerRelated? tryParse(String s) {
switch (s.toUpperCase()) {
case start:
return TriggerRelated.start;
case end:
return TriggerRelated.end;
default:
return null;
}
}
}

/// Extensions for [TriggerRelated] enum.
extension TriggerRelatedExtensions on TriggerRelated {
/// Converts the enum value to its corresponding string name.
String toName() => switch (this) {
TriggerRelated.start => TriggerRelatedNames.start,
TriggerRelated.end => TriggerRelatedNames.end,
};
}

/// Value types.
enum ValueType {
binary,
Expand Down
20 changes: 16 additions & 4 deletions lib/src/semantic/calendar_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -836,11 +836,21 @@ class Trigger {
/// The date-time value, if specified.
final CalDateTime? dateTime;

/// The RELATED parameter as enum value, if explicitly set.
final TriggerRelated? related;

/// The RELATED parameter as raw string, if explicitly set.
final String? relatedName;

/// Creates a Trigger with a DURATION.
const Trigger.duration(CalDuration this.duration) : dateTime = null;
const Trigger.duration(
CalDuration this.duration, {
this.related,
this.relatedName,
}) : dateTime = null;

/// Creates a Trigger with a UTC DATE-TIME.
Trigger.dateTime(CalDateTime this.dateTime)
Trigger.dateTime(CalDateTime this.dateTime, {this.related, this.relatedName})
: duration = null,
assert(dateTime.isDateTime, 'time part must be present'),
assert(dateTime.time!.isUtc);
Expand All @@ -857,12 +867,14 @@ class Trigger {

return other is Trigger &&
other.duration == duration &&
other.dateTime == dateTime;
other.dateTime == dateTime &&
other.related == related &&
other.relatedName == relatedName;
}

@override
int get hashCode {
return Object.hash(duration, dateTime);
return Object.hash(duration, dateTime, related, relatedName);
}

@override
Expand Down
39 changes: 33 additions & 6 deletions lib/src/semantic/property_parsers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -339,14 +339,34 @@ CalTime parseCalTimeValue(String value, {String? tzid, int lineNumber = 0}) {

/// Parses a [CalendarProperty] into a trigger value.
Trigger parseTrigger(CalendarProperty property) {
final relatedName = _tryParseSingleParameterValue(
property,
parameterName: 'RELATED',
);
final related = relatedName != null
? TriggerRelatedNames.tryParse(relatedName) ??
(throw ParseException(
'Invalid RELATED value "$relatedName" for property "${property.name}"',
lineNumber: property.lineNumber,
))
: null;

final valueType =
property.parameters['VALUE']?.first ?? ValueTypeNames.duration;
if (valueType == ValueTypeNames.duration) {
Comment on lines 354 to 357
final duration = parseCalDuration(property);
return Trigger.duration(duration);
return Trigger.duration(
duration,
related: related,
relatedName: relatedName,
);
} else if (valueType == ValueTypeNames.dateTime) {
final dateTime = parseCalDateTimeUtc(property);
return Trigger.dateTime(dateTime);
return Trigger.dateTime(
dateTime,
related: related,
Comment on lines 364 to +375
relatedName: relatedName,
);
}
throw ParseException(
'Invalid value type "$valueType" for property "${property.name}"',
Expand Down Expand Up @@ -376,14 +396,21 @@ UtcOffset parseUtcOffset(CalendarProperty property) {
}

String? _tryParseTzidParameter(CalendarProperty property) {
final tzidList = property.parameters['TZID'];
if (tzidList != null && tzidList.length > 1) {
return _tryParseSingleParameterValue(property, parameterName: 'TZID');
}

String? _tryParseSingleParameterValue(
CalendarProperty property, {
required String parameterName,
}) {
final values = property.parameters[parameterName];
if (values != null && values.length > 1) {
throw ParseException(
'Multiple TZID parameters found for property "${property.name}"',
'Multiple $parameterName parameters found for property "${property.name}"',
lineNumber: property.lineNumber,
);
}
return tzidList?.firstOrNull;
return values?.firstOrNull;
}

String _parseTextString(String value) {
Expand Down
11 changes: 11 additions & 0 deletions test/semantic/calendar_types_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,17 @@ void main() {

expect(trigger.toString(), equals('20250101T100000Z'));
});

test('stores RELATED parameter metadata', () {
final trigger = Trigger.duration(
CalDuration(minutes: 15),
related: TriggerRelated.end,
relatedName: 'END',
);

expect(trigger.related, TriggerRelated.end);
expect(trigger.relatedName, 'END');
});
});

group('UtcOffset', () {
Expand Down
2 changes: 2 additions & 0 deletions test/semantic/end_to_end_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,8 @@ void main() {
);

expect(alarm.trigger.duration, CalDuration(sign: Sign.negative, days: 2));
expect(alarm.trigger.related, TriggerRelated.end);
expect(alarm.trigger.relatedName, 'END');
expect(alarm.action, AlarmAction.email);
expect(alarm.attendees, isNotEmpty);
expect(alarm.attendees.first.address, 'mailto:john_doe@example.com');
Expand Down
47 changes: 47 additions & 0 deletions test/semantic/property_parser_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,53 @@ void main() {
expect(trigger.dateTime, CalDateTime.utc(1998, 1, 1, 12, 0, 0));
});

test('Parse TRIGGER with RELATED=END', () {
final property = DocumentParser.parseProperty(
'TRIGGER;RELATED=END:-PT15M',
);
final trigger = parseTrigger(property);
expect(trigger.duration, CalDuration(sign: Sign.negative, minutes: 15));
expect(trigger.related, TriggerRelated.end);
expect(trigger.relatedName, 'END');
});

test('Parse TRIGGER with invalid RELATED value throws', () {
final property = DocumentParser.parseProperty(
'TRIGGER;RELATED=INVALID:-PT15M',
);
expect(
() => parseTrigger(property),
throwsA(
isA<ParseException>().having(
(e) => e.message,
'message',
'Invalid RELATED value "INVALID" for property "TRIGGER"',
),
),
);
});

test('Parse TRIGGER with multiple RELATED parameters throws', () {
final property = CalendarProperty(
name: 'TRIGGER',
value: '-PT15M',
parameters: {
'RELATED': ['START', 'END'],
},
lineNumber: 1,
);
expect(
() => parseTrigger(property),
throwsA(
isA<ParseException>().having(
(e) => e.message,
'message',
'Multiple RELATED parameters found for property "TRIGGER"',
),
),
);
});

test('Parse TRIGGER with invalid VALUE type throws', () {
final property = DocumentParser.parseProperty(
'TRIGGER;VALUE=TEXT:invalid',
Expand Down
Loading