Skip to content

Commit f6b1cab

Browse files
committed
Formatters & Validators #2
1 parent c713437 commit f6b1cab

File tree

5 files changed

+364
-57
lines changed

5 files changed

+364
-57
lines changed

lib/src/api/extensions.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ extension StringExtensions on String {
1818
return variablePath.replaceFirst('\${$oldName', '\${$newName');
1919
});
2020
}
21+
22+
/// Returns true if this string exactly matches the given [pattern].
23+
bool hasExactMatch(Pattern pattern) => pattern.hasExactMatch(this);
2124
}
2225

2326
/// A helper extension that adds additional functionality to [Iterable].
@@ -64,3 +67,13 @@ extension RoundNum on num {
6467
return num.parse(_formatter.format(this));
6568
}
6669
}
70+
71+
/// A helper extension that adds additional functionality to [Pattern].
72+
extension PatternExt on Pattern {
73+
/// Returns true if [input] exactly matches the patten.
74+
bool hasExactMatch(String? input) {
75+
final Match? match = matchAsPrefix(input ??= '');
76+
if (match == null) return false;
77+
return match.start == 0 && match.end == input.length;
78+
}
79+
}
Lines changed: 257 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,303 @@
11
import 'package:codelessly_json_annotation/codelessly_json_annotation.dart';
22
import 'package:equatable/equatable.dart';
3+
import 'package:meta/meta.dart' hide required;
34

5+
import '../extensions.dart';
46
import '../mixins.dart';
57

68
part 'text_input_validator_model.g.dart';
79

8-
/// Text formatters that can be applied to the text field input to restrict the
9-
/// input to a specific format.
10+
/// Represents the type of validator.
11+
enum TextInputValidatorType {
12+
/// Represents absence of any validation.
13+
none,
14+
15+
/// Represents a regular expression based text input validator.
16+
regex,
17+
18+
/// Represents a validator for URL input.
19+
url,
20+
}
21+
22+
/// Represents a text input validator.
23+
sealed class TextInputValidatorModel with EquatableMixin, SerializableMixin {
24+
/// The name of the validator. This is used to identify the validator.
25+
final String name;
26+
27+
/// Whether the input is required. Defaults to `false`. If `true`, the input
28+
/// must not be empty.
29+
final bool required;
30+
31+
/// The type of validator.
32+
@JsonKey(includeToJson: true, required: true)
33+
final TextInputValidatorType type;
34+
35+
/// Creates a new [TextInputValidatorModel] instance.
36+
const TextInputValidatorModel({
37+
required this.name,
38+
this.required = false,
39+
required this.type,
40+
});
41+
42+
/// Creates a [TextInputValidatorModel] instance from a JSON object.
43+
factory TextInputValidatorModel.fromJson(Map<String, dynamic> json) {
44+
final TextInputValidatorType type =
45+
TextInputValidatorType.values.byName(json['type']);
46+
return switch (type) {
47+
TextInputValidatorType.none => NoneTextInputValidatorModel.fromJson(json),
48+
TextInputValidatorType.regex =>
49+
RegexTextInputValidatorModel.fromJson(json),
50+
TextInputValidatorType.url => UrlTextInputValidatorModel.fromJson(json)
51+
};
52+
}
53+
54+
/// Allows objects of this class and its subclasses to be used as a function.
55+
@internal
56+
String? call(String? input) => validate(input);
57+
58+
/// Validates the input and returns an error message if the input is invalid.
59+
/// Returns `null` if the input is valid.
60+
/// This method should be overridden by the subclasses.
61+
String? validate(String? input);
62+
63+
@override
64+
List<Object?> get props => [name, type, required];
65+
66+
/// A complete list of built-in validators.
67+
static const List<TextInputValidatorModel> validators = [
68+
NoneTextInputValidatorModel(),
69+
...RegexTextInputValidatorModel.validators,
70+
UrlTextInputValidatorModel(),
71+
];
72+
}
73+
74+
/// Represents absence of any validation.
75+
// TODO: should this be removed and the validator field be nullable?
76+
@JsonSerializable()
77+
class NoneTextInputValidatorModel extends TextInputValidatorModel {
78+
/// Creates a new [NoneTextInputValidatorModel] instance.
79+
const NoneTextInputValidatorModel()
80+
: super(name: 'None', type: TextInputValidatorType.none);
81+
82+
@override
83+
String? validate(String? input) => null;
84+
85+
/// Creates a [TextInputValidatorModel] instance from a JSON object.
86+
factory NoneTextInputValidatorModel.fromJson(Map<String, dynamic> json) =>
87+
_$NoneTextInputValidatorModelFromJson(json);
88+
89+
@override
90+
Map toJson() => _$NoneTextInputValidatorModelToJson(this);
91+
}
92+
93+
/// Represents the type of validation to perform.
94+
enum RegexValidationType {
95+
/// Allow the input if it matches the pattern.
96+
allow,
97+
98+
/// Deny the input if it matches the pattern.
99+
deny,
100+
}
101+
102+
/// Represents a regular expression based text input validator.
10103
@JsonSerializable()
11-
class TextInputValidatorModel with EquatableMixin, SerializableMixin {
104+
class RegexTextInputValidatorModel extends TextInputValidatorModel {
12105
/// The regular expression to match the text.
13106
final String pattern;
14107

15-
/// The name of the formatter.
16-
final String name;
17-
18-
/// The error message to show when the input is invalid.
108+
/// The error message to show when the input is invalid. Override the default
109+
/// [validate] method to have different/multiple error messages on different
110+
/// conditions.
19111
final String errorMessage;
20112

21-
/// Allow only digits in the text field.
22-
static const TextInputValidatorModel none = TextInputValidatorModel(
23-
name: 'None',
24-
pattern: r'.*',
25-
errorMessage: '',
26-
);
113+
/// Whether the pattern matching should be case sensitive.
114+
/// Defaults to `true`.
115+
final bool caseSensitive;
116+
117+
/// Whether the `.` pattern should match all characters, including
118+
/// line terminators.
119+
/// Defaults to `false`.
120+
final bool dotAll;
121+
122+
/// Whether the pattern should match across multiple lines.
123+
/// Defaults to `false`.
124+
final bool multiLine;
125+
126+
/// Whether the pattern should match unicode characters.
127+
/// Defaults to `false`.
128+
final bool unicode;
129+
130+
/// The type of validation to perform. Default is [RegexValidationType.allow].
131+
final RegexValidationType validationType;
27132

28133
/// Allow only digits in the text field.
29-
static const TextInputValidatorModel digitsOnly = TextInputValidatorModel(
134+
static const RegexTextInputValidatorModel digitsOnly =
135+
RegexTextInputValidatorModel(
30136
name: 'Digits Only',
31137
pattern: r'[0-9]',
32138
errorMessage: 'Only digits are allowed.',
33139
);
34140

35-
/// List of all available text field formatters.
36-
static const List<TextInputValidatorModel> values = [
37-
none,
38-
digitsOnly,
39-
];
141+
/// Allow only alphabets in the text field.
142+
static const RegexTextInputValidatorModel alphabetsOnly =
143+
RegexTextInputValidatorModel(
144+
name: 'Alphabets Only',
145+
pattern: r'[a-zA-Z]',
146+
errorMessage: 'Only alphabets are allowed.',
147+
);
40148

41-
/// Returns true if the formatter is [none].
42-
bool get isNone => this == none;
149+
/// Allow only alphabets and digits in the text field.
150+
static const RegexTextInputValidatorModel alphanumeric =
151+
RegexTextInputValidatorModel(
152+
name: 'Alphanumeric',
153+
pattern: r'[a-zA-Z0-9]',
154+
errorMessage: 'Only alphabets and digits are allowed.',
155+
);
43156

44-
/// Creates a new [TextInputValidatorModel] instance with the given pattern.
45-
const TextInputValidatorModel({
46-
required this.name,
157+
/// A validator for email addresses.
158+
static const RegexTextInputValidatorModel email =
159+
RegexTextInputValidatorModel(
160+
name: 'Email',
161+
pattern: r'^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$',
162+
errorMessage: 'Invalid email address.',
163+
);
164+
165+
/// A validator for phone numbers.
166+
static const RegexTextInputValidatorModel phoneNumber =
167+
RegexTextInputValidatorModel(
168+
name: 'Phone Number',
169+
pattern:
170+
r'^\+?([0-9]{1,4})\)?[-. ]?([0-9]{1,4})[-. ]?([0-9]{1,4})[-. ]?([0-9]{1,4})$',
171+
errorMessage: 'Invalid phone number.',
172+
);
173+
174+
/// A list of built-in regex validators.
175+
static const List<RegexTextInputValidatorModel> validators = [
176+
RegexTextInputValidatorModel.digitsOnly,
177+
RegexTextInputValidatorModel.alphabetsOnly,
178+
RegexTextInputValidatorModel.alphanumeric,
179+
RegexTextInputValidatorModel.email,
180+
RegexTextInputValidatorModel.phoneNumber,
181+
];
182+
183+
/// Creates a new [RegexTextInputValidatorModel] instance with the given pattern.
184+
const RegexTextInputValidatorModel({
185+
required super.name,
47186
required this.pattern,
48187
required this.errorMessage,
49-
});
188+
this.caseSensitive = true,
189+
this.dotAll = false,
190+
this.multiLine = false,
191+
this.unicode = false,
192+
this.validationType = RegexValidationType.allow,
193+
super.required,
194+
}) : super(type: TextInputValidatorType.regex);
50195

51-
/// Validates the input against the pattern. This can be used to use
52-
/// this class as a function.
53-
String? call(String? value) => validate(value);
54-
55-
/// Validates the input against the pattern.
196+
@override
56197
String? validate(String? input) {
57-
// check if the input perfectly matches the pattern.
58-
if (RegExp(pattern).firstMatch(input ?? '')
59-
case RegExpMatch(start: var start, end: var end)) {
60-
if (start == 0 && end == (input?.length ?? 0)) {
61-
// A perfect match. This is valid.
62-
return null;
63-
}
198+
if (input == null) {
199+
if (required) return 'This field is required.';
200+
return null;
64201
}
202+
final bool hasExactMatch = regex.hasExactMatch(input);
203+
final bool isValid = switch (validationType) {
204+
RegexValidationType.allow => hasExactMatch,
205+
RegexValidationType.deny => !hasExactMatch,
206+
};
207+
208+
if (isValid) return null;
65209
return errorMessage;
66210
}
67211

68-
/// copyWith
69-
TextInputValidatorModel copyWith({
212+
/// Builds a [RegExp] instance from the [pattern] and other properties like
213+
/// [caseSensitive], [dotAll], [multiLine], and [unicode].
214+
RegExp get regex => RegExp(
215+
pattern,
216+
caseSensitive: caseSensitive,
217+
dotAll: dotAll,
218+
multiLine: multiLine,
219+
unicode: unicode,
220+
);
221+
222+
/// Creates a copy of this instance with the given properties replaced.
223+
RegexTextInputValidatorModel copyWith({
70224
String? name,
71225
String? pattern,
72226
String? errorMessage,
227+
bool? caseSensitive,
228+
bool? dotAll,
229+
bool? multiLine,
230+
bool? unicode,
231+
RegexValidationType? validationType,
232+
bool? required,
73233
}) {
74-
return TextInputValidatorModel(
234+
return RegexTextInputValidatorModel(
75235
name: name ?? this.name,
76236
pattern: pattern ?? this.pattern,
77237
errorMessage: errorMessage ?? this.errorMessage,
238+
caseSensitive: caseSensitive ?? this.caseSensitive,
239+
dotAll: dotAll ?? this.dotAll,
240+
multiLine: multiLine ?? this.multiLine,
241+
unicode: unicode ?? this.unicode,
242+
validationType: validationType ?? this.validationType,
243+
required: required ?? this.required,
78244
);
79245
}
80246

81247
/// Creates a [TextInputValidatorModel] instance from a JSON object.
82-
factory TextInputValidatorModel.fromJson(Map json) =>
83-
_$TextInputValidatorModelFromJson(json);
248+
factory RegexTextInputValidatorModel.fromJson(Map json) =>
249+
_$RegexTextInputValidatorModelFromJson(json);
250+
251+
@override
252+
Map toJson() => _$RegexTextInputValidatorModelToJson(this);
253+
254+
@override
255+
List<Object?> get props => [
256+
...super.props,
257+
pattern,
258+
errorMessage,
259+
caseSensitive,
260+
dotAll,
261+
multiLine,
262+
unicode,
263+
validationType,
264+
];
265+
}
266+
267+
/// A custom validator that takes a callback function.
268+
@JsonSerializable()
269+
class UrlTextInputValidatorModel extends TextInputValidatorModel {
270+
/// Custom error message to show when the input is invalid.
271+
final String? errorMessage;
272+
273+
/// Creates a new [UrlTextInputValidatorModel] instance.
274+
const UrlTextInputValidatorModel({
275+
this.errorMessage,
276+
super.required,
277+
}) : super(
278+
name: 'URL',
279+
type: TextInputValidatorType.url,
280+
);
281+
282+
@override
283+
String? validate(String? input) {
284+
if (input == null) {
285+
if (required) return 'This field is required.';
286+
return null;
287+
}
288+
if (Uri.tryParse(input)?.isAbsolute == true) {
289+
return null;
290+
}
291+
return errorMessage ?? 'Invalid URL.';
292+
}
293+
294+
/// Creates a [TextInputValidatorModel] instance from a JSON object.
295+
factory UrlTextInputValidatorModel.fromJson(Map json) =>
296+
_$UrlTextInputValidatorModelFromJson(json);
84297

85298
@override
86-
Map toJson() => _$TextInputValidatorModelToJson(this);
299+
Map toJson() => _$UrlTextInputValidatorModelToJson(this);
87300

88301
@override
89-
List<Object?> get props => [name];
302+
List<Object?> get props => [...super.props, errorMessage];
90303
}

0 commit comments

Comments
 (0)