Skip to content

Commit f9e3d65

Browse files
authored
Merge pull request #7 from comatory/provide-additional-options-for-constructing-urls
Provide additional options for constructing urls
2 parents 03af5d1 + 2141905 commit f9e3d65

10 files changed

Lines changed: 258 additions & 15 deletions

File tree

example/urldat_example.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,24 @@ void main() {
4646
assert(urldatConfig('/:id/posts/:postId',
4747
parameters: {'id': 10, 'postId': 200}) ==
4848
'https://api.example.com/v1/users/10/posts/200');
49+
50+
/* If you want more convenience, pass `scheme` like http/https
51+
as an option.
52+
*/
53+
assert(urldat('dart.dev', '/:section',
54+
parameters: {'section': 'search'}, scheme: 'https') ==
55+
'https://dart.dev/search');
56+
57+
/* You can also specify port for base path */
58+
assert(urldat('dart.dev', '/:section',
59+
parameters: {'section': 'search'}, scheme: 'https', port: 3000) ==
60+
'https://dart.dev:3000/search');
61+
62+
/* You can also specify fragment for path */
63+
assert(urldat('dart.dev', '/:section',
64+
parameters: {'section': 'search'},
65+
scheme: 'https',
66+
port: 3000,
67+
fragment: 'hello') ==
68+
'https://dart.dev:3000/search#hello');
4969
}

lib/src/errors/errors.dart

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,42 @@
11
/// Use this to detect when [urldat.urldat] throws due to invalid
22
/// combination of inputs.
3+
/// If you want to detect types of error programatically, use [id] property
4+
/// which will give you [UrldatErrorId] identifier.
35
class UrldatError implements Exception {
4-
UrldatError(String message) {
6+
UrldatError(String message, UrldatErrorId id) {
57
_message = message;
8+
_id = id;
69
}
10+
UrldatError.basePathSchemeError()
11+
: _message = 'Base path already contains a scheme. Remove scheme option.',
12+
_id = UrldatErrorId.basePathSchemeError;
13+
UrldatError.pathFragmentError()
14+
: _message =
15+
'Base path already contains a fragment. Remove fragment option.',
16+
_id = UrldatErrorId.pathFragmentError;
17+
UrldatError.missingParametersWithTemplateError()
18+
: _message = 'When using path templates, you must pass parameters map.',
19+
_id = UrldatErrorId.missingParametersWithTemplateError;
20+
UrldatError.emptyParametersWithTemplateError()
21+
: _message =
22+
'When using path templates, you must pass non-empty parameters map.',
23+
_id = UrldatErrorId.emptyParametersWithTemplateError;
724

8-
String _message = '';
25+
late String _message;
26+
late UrldatErrorId _id;
27+
28+
UrldatErrorId get id => _id;
929

1030
@override
1131
String toString() {
12-
return _message;
32+
return [_id, _message].join('\n');
1333
}
1434
}
35+
36+
/// ID for catching type of errors programatically
37+
enum UrldatErrorId {
38+
basePathSchemeError,
39+
pathFragmentError,
40+
missingParametersWithTemplateError,
41+
emptyParametersWithTemplateError,
42+
}

lib/src/typedefs.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
typedef UrldatConfiguredFn = String Function(
22
String pathOrTemplate, {
33
Map<String, dynamic>? parameters,
4+
String? scheme,
5+
int? port,
6+
String? fragment,
47
});
58

69
typedef UrldatFn = String Function(
710
String base,
811
String pathOrTemplate, {
912
Map<String, dynamic>? parameters,
13+
String? scheme,
14+
int? port,
15+
String? fragment,
1016
});

lib/src/urldat.dart

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,48 @@ import './errors/errors.dart';
1212
/// Extra keys in [parameters] that do not map to [pathOrTemplate] are
1313
/// always used as query parameters.
1414
///
15-
/// If [pathOrTemplate] is template (contains `:`) and no [parameters] are
15+
/// If [pathOrTemplate] is template (contains `:colon`) and no [parameters] are
1616
/// provided, [UrldatError] exception will be thrown.
17-
String urldat(
18-
String base,
19-
String pathOrTemplate, {
20-
Map<String, dynamic>? parameters,
21-
}) {
17+
///
18+
/// [scheme] option is a valid URI scheme. It's intended to be used
19+
/// with [base] URL when no scheme is present in it.
20+
/// Warning: Function will throw [UrldatError] when [base] path contains
21+
/// scheme already.
22+
///
23+
/// [port] option is a integer describing the port number, zero or 80 are
24+
/// ignored in the final URL
25+
///
26+
/// [fragment] option will append fragment to URL. When URL already contains
27+
/// fragment value, [UrldatError] will be thrown
28+
String urldat(String base, String pathOrTemplate,
29+
{Map<String, dynamic>? parameters,
30+
String? scheme,
31+
int? port,
32+
String? fragment}) {
33+
final uri = Uri.parse(base);
34+
35+
if (hasScheme(uri) && scheme != null) {
36+
throw UrldatError.basePathSchemeError();
37+
}
38+
39+
if (hasFragment(pathOrTemplate) && fragment != null) {
40+
throw UrldatError.pathFragmentError();
41+
}
42+
43+
final parsedScheme = scheme != null ? Uri(scheme: scheme).scheme : null;
44+
2245
final sanitizedBase = removeTrailingSlash(base);
2346
final sanitizedPathWithoutSlash =
2447
removeLeadingAndTrailingSlash(pathOrTemplate);
2548
final sanitizedPath = removeTrailingQuestionMark(sanitizedPathWithoutSlash);
2649

2750
if (isTemplate(sanitizedPath)) {
2851
if (parameters == null) {
29-
throw UrldatError(
30-
'When using path templates, you must pass parameters map.');
52+
throw UrldatError.missingParametersWithTemplateError();
3153
}
3254

3355
if (parameters.isEmpty) {
34-
throw UrldatError(
35-
'When using path templates, you must pass non-empty parameters map.');
56+
throw UrldatError.emptyParametersWithTemplateError();
3657
}
3758

3859
final templateKeys = getTemplateKeys(sanitizedPath);
@@ -48,6 +69,9 @@ String urldat(
4869
base: sanitizedBase,
4970
path: filledTemplate,
5071
query: queryParameters,
72+
scheme: parsedScheme,
73+
port: port,
74+
fragment: fragment,
5175
);
5276
}
5377

@@ -57,5 +81,8 @@ String urldat(
5781
base: sanitizedBase,
5882
path: sanitizedPath,
5983
query: queryParameters,
84+
scheme: parsedScheme,
85+
port: port,
86+
fragment: fragment,
6087
);
6188
}

lib/src/urldat_factory.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ UrldatConfiguredFn urldatFactory(String base) {
1313
return (
1414
String pathOrTemplate, {
1515
Map<String, dynamic>? parameters,
16+
String? scheme,
17+
int? port,
18+
String? fragment,
1619
}) =>
1720
urldat(base, pathOrTemplate, parameters: parameters);
1821
}

lib/src/utils/converters.dart

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,26 @@ String joinParts({
22
required String base,
33
required String path,
44
required String query,
5+
String? scheme,
6+
int? port,
7+
String? fragment,
58
}) {
9+
final schemePart = scheme != null ? '$scheme://' : '';
10+
final portPart = (port != null && port != 0 && port != 80) ? ':$port' : '';
11+
final fragmentPart = fragment != null ? '#$fragment' : '';
12+
final baseWithSchemeAndPort = '$schemePart$base$portPart';
13+
614
final tail = [path, query].join();
15+
final tailWithFragment = '$tail$fragmentPart';
716

817
/* NOTE: In case path or template is empty string, tail becomes just
918
query. Do not add slash but join parts together as they are
1019
*/
1120
if (tail.startsWith('?')) {
12-
return [base, tail].join();
21+
return [baseWithSchemeAndPort, tailWithFragment].join();
1322
}
1423

15-
return [base, tail].join('/');
24+
return [baseWithSchemeAndPort, tailWithFragment].join('/');
1625
}
1726

1827
Map<String, String> stringifyValuesInMap(Map<String, dynamic> map) {

lib/src/utils/detectors.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
final templateRegex = RegExp(r'(:{1}[^:/]+)');
2+
final fragmentRegex = RegExp(r'^.*\#\S*$');
23

34
bool isTemplate(String path) {
45
return templateRegex.hasMatch(path);
56
}
7+
8+
bool hasScheme(Uri uri) {
9+
return uri.hasScheme;
10+
}
11+
12+
bool hasFragment(String path) {
13+
return fragmentRegex.hasMatch(path);
14+
}

test/urldat_test.dart

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,45 @@ void main() {
146146
equals('http://localhost:3000/path/test'));
147147
});
148148
});
149+
150+
group('additional options', () {
151+
test('should create URL with defined scheme', () {
152+
expect(
153+
urldat('dart.dev', '/path/:p',
154+
parameters: {'p': 'test'}, scheme: 'https'),
155+
equals('https://dart.dev/path/test'));
156+
});
157+
158+
test(
159+
'should throw error when scheme option is defined and'
160+
'URL contains scheme as well', () {
161+
expect(
162+
() => urldat('https://dart.dev', '/path/:p',
163+
parameters: {'p': 'test'}, scheme: 'https'),
164+
throwsA(const TypeMatcher<UrldatError>()));
165+
});
166+
167+
test('should create URL with defined port', () {
168+
expect(
169+
urldat('localhost', '/path/:p',
170+
parameters: {'p': 'test'}, port: 4000),
171+
equals('localhost:4000/path/test'));
172+
});
173+
174+
test('should create URL with defined fragment', () {
175+
expect(
176+
urldat('https://dart.dev', '/path/:p',
177+
parameters: {'p': 'test'}, fragment: 'about'),
178+
equals('https://dart.dev/path/test#about'));
179+
});
180+
181+
test(
182+
'should throw error when fragment option is defined and'
183+
'URL contains fragment as well', () {
184+
expect(
185+
() => urldat('https://dart.dev', '/path/:p#about',
186+
parameters: {'p': 'test'}, fragment: 'about'),
187+
throwsA(const TypeMatcher<UrldatError>()));
188+
});
189+
});
149190
}

test/utils/converters_test.dart

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,86 @@ void main() {
4444
),
4545
equals('http://example.com/path'));
4646
});
47+
48+
test('should create base with passed in scheme', () {
49+
expect(
50+
joinParts(
51+
scheme: Uri(scheme: 'ftp').scheme,
52+
base: 'example.com',
53+
path: 'path',
54+
query: '',
55+
),
56+
equals('ftp://example.com/path'));
57+
});
58+
59+
test('should create base with port', () {
60+
expect(
61+
joinParts(
62+
port: 4004,
63+
base: 'example.com',
64+
path: 'path',
65+
query: '',
66+
),
67+
equals('example.com:4004/path'));
68+
});
69+
70+
test('should ignore port 80', () {
71+
expect(
72+
joinParts(
73+
port: 80,
74+
base: 'example.com',
75+
path: 'path',
76+
query: '',
77+
),
78+
equals('example.com/path'));
79+
});
80+
81+
test('should ignore port 0', () {
82+
expect(
83+
joinParts(
84+
port: 0,
85+
base: 'example.com',
86+
path: 'path',
87+
query: '',
88+
),
89+
equals('example.com/path'));
90+
});
91+
92+
test('should create base with port and scheme and query', () {
93+
expect(
94+
joinParts(
95+
port: 9911,
96+
scheme: 'ftp',
97+
base: 'example.com',
98+
path: 'path',
99+
query: '?search=what&category=1',
100+
),
101+
equals('ftp://example.com:9911/path?search=what&category=1'));
102+
});
103+
104+
test('should create URL with fragment', () {
105+
expect(
106+
joinParts(
107+
base: 'example.com',
108+
path: 'path/to/somewhere',
109+
query: '',
110+
fragment: 'about-me'),
111+
equals('example.com/path/to/somewhere#about-me'));
112+
});
113+
114+
test('should create base with port, scheme, query and fragment', () {
115+
expect(
116+
joinParts(
117+
port: 9911,
118+
scheme: 'ftp',
119+
base: 'example.com',
120+
path: 'path',
121+
fragment: 'section-1',
122+
query: '?search=what&category=1',
123+
),
124+
equals(
125+
'ftp://example.com:9911/path?search=what&category=1#section-1'));
126+
});
47127
});
48128

49129
group('stringifyValuesInMap', () {

test/utils/detectors_test.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,24 @@ void main() {
1212
expect(isTemplate('/path'), isFalse);
1313
});
1414
});
15+
16+
group('hasScheme', () {
17+
test('should detect that URI has scheme', () {
18+
expect(hasScheme(Uri.parse('https://dart.dev')), isTrue);
19+
});
20+
21+
test('should NOT detect that URI has scheme', () {
22+
expect(hasScheme(Uri.parse('dart.dev')), isFalse);
23+
});
24+
});
25+
26+
group('hasFragment', () {
27+
test('should detect that path has fragment', () {
28+
expect(hasFragment('/path/:section#fragment'), isTrue);
29+
});
30+
31+
test('should NOT detect that path has fragment', () {
32+
expect(hasFragment('/path/:section'), isFalse);
33+
});
34+
});
1535
}

0 commit comments

Comments
 (0)