Skip to content

Commit 714e65c

Browse files
feat: Include Paths Parameter (#422)
1 parent 45d34ea commit 714e65c

File tree

7 files changed

+229
-1
lines changed

7 files changed

+229
-1
lines changed

swagger_parser/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 1.40.0
2+
- Add `include_paths` option for filtering endpoints by paths
3+
14
## 1.39.2
25
- Add `generate_urls_constants` option for generating URL constants for all endpoints
36

swagger_parser/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@ swagger_parser:
159159
- pattern: "\\]"
160160
replacement: "_"
161161

162+
# Optional. Set paths to be included on endpoint generation.
163+
#
164+
# Also supports wildcard paths (e.g. `/path/*/update` or `/path/**`)
165+
#
166+
# If set, only endpoints with these paths will be included in the generated clients.
167+
include_paths:
168+
- "/some/concrete/path/{id}"
169+
- "/some/wildcard/*/path"
170+
- "/another/wildcard/**"
171+
162172
# Optional. Skip parameters with names.
163173
skipped_parameters:
164174
- "X-Some-Token"

swagger_parser/lib/src/config/swp_config.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class SWPConfig {
4343
this.dartMappableConvenientWhen = false,
4444
this.excludeTags = const <String>[],
4545
this.includeTags = const <String>[],
46+
this.includePaths,
4647
this.fallbackClient = 'fallback',
4748
this.mergeOutputs = false,
4849
this.includeIfNull = false,
@@ -84,6 +85,7 @@ class SWPConfig {
8485
required this.useMultipartFile,
8586
required this.excludeTags,
8687
required this.includeTags,
88+
required this.includePaths,
8789
required this.fallbackClient,
8890
required this.mergeOutputs,
8991
required this.dartMappableConvenientWhen,
@@ -311,6 +313,22 @@ class SWPConfig {
311313
includedTags = List.from(rootConfig!.includeTags);
312314
}
313315

316+
final includePaths = yamlMap['include_paths'] as YamlList?;
317+
List<String>? includePathsList;
318+
if (includePaths != null) {
319+
includePathsList = [];
320+
for (final p in includePaths) {
321+
if (p is! String) {
322+
throw const ConfigException(
323+
"Config parameter 'include_paths' values must be List of String.",
324+
);
325+
}
326+
includePathsList.add(p);
327+
}
328+
} else if (rootConfig?.includePaths case final paths?) {
329+
includePathsList = List.from(paths);
330+
}
331+
314332
final fallbackClient =
315333
yamlMap['fallback_client'] as String? ?? rootConfig?.fallbackClient;
316334

@@ -376,6 +394,7 @@ class SWPConfig {
376394
inferRequiredFromNullable:
377395
inferRequiredFromNullable ?? dc.inferRequiredFromNullable,
378396
useFlutterCompute: useFlutterCompute ?? dc.useFlutterCompute,
397+
includePaths: includePathsList ?? dc.includePaths,
379398
generateUrlsConstants: generateUrlsConstants ?? dc.generateUrlsConstants,
380399
);
381400
}
@@ -550,6 +569,15 @@ class SWPConfig {
550569
/// Endpoints with these tags will not be included in the generated clients.
551570
final List<String> excludeTags;
552571

572+
/// {@template include_paths}
573+
/// Optional. Set included paths.
574+
///
575+
/// Also supports wildcard paths (e.g. `/path/*/update` or `/path/**`)
576+
///
577+
/// If set, only endpoints with these paths will be included in the generated clients.
578+
/// {@endtemplate}
579+
final List<String>? includePaths;
580+
553581
/// DART ONLY
554582
/// Optional. Set included tags.
555583
///
@@ -641,6 +669,7 @@ class SWPConfig {
641669
excludeTags: excludeTags,
642670
replacementRulesForRawSchema: replacementRulesForRawSchema,
643671
includeTags: includeTags,
672+
includePaths: includePaths,
644673
fallbackClient: fallbackClient,
645674
inferRequiredFromNullable: inferRequiredFromNullable,
646675
);

swagger_parser/lib/src/parser/config/parser_config.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class ParserConfig {
1717
this.useXNullable = false,
1818
this.excludeTags = const <String>[],
1919
this.includeTags = const <String>[],
20+
this.includePaths,
2021
this.fallbackClient = 'fallback',
2122
this.inferRequiredFromNullable = false,
2223
});
@@ -74,6 +75,9 @@ class ParserConfig {
7475
/// **NOTE: This will override the [excludeTags] if set.**
7576
final List<String> includeTags;
7677

78+
///{@macro include_paths}
79+
final List<String>? includePaths;
80+
7781
/// DART ONLY
7882
/// Optional. Fallback client name for endpoints without tags.
7983
///

swagger_parser/lib/src/parser/parser/open_api_parser.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import 'package:swagger_parser/src/parser/model/universal_type.dart';
1717
import 'package:swagger_parser/src/parser/utils/anchor_registry.dart';
1818
import 'package:swagger_parser/src/parser/utils/context_stack.dart';
1919
import 'package:swagger_parser/src/parser/utils/http_utils.dart';
20+
import 'package:swagger_parser/src/parser/utils/path_match.dart';
2021
import 'package:swagger_parser/src/parser/utils/type_utils.dart';
2122
import 'package:yaml/yaml.dart';
2223

@@ -596,10 +597,17 @@ class OpenApiParser {
596597
if (!_definitionFileContent.containsKey(_pathsConst)) {
597598
return [];
598599
}
600+
601+
final includePaths = config.includePaths;
602+
599603
(_definitionFileContent[_pathsConst] as Map<String, dynamic>).forEach((
600604
path,
601605
pathValue,
602606
) {
607+
if (includePaths != null && !matchesPathPattern(path, includePaths)) {
608+
return;
609+
}
610+
603611
final pathValueMap = pathValue as Map<String, dynamic>;
604612

605613
// global parameters are defined at the path level (i.e. /users/{id})
@@ -1111,7 +1119,9 @@ class OpenApiParser {
11111119
}
11121120
}
11131121

1114-
if (config.includeTags.isNotEmpty || config.excludeTags.isNotEmpty) {
1122+
if (config.includeTags.isNotEmpty ||
1123+
config.excludeTags.isNotEmpty ||
1124+
config.includePaths != null) {
11151125
return _filterUsedClasses(dataClasses);
11161126
}
11171127

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/// Checks if the given [path] matches any of the provided [patterns].
2+
/// Supports wildcard patterns:
3+
/// - * matches any characters except forward slashes (one path segment)
4+
/// - ** matches any characters including forward slashes
5+
///
6+
/// Some examples:
7+
/// - 'some/concrete/path/{id}' matches 'some/concrete/path/{id}'
8+
/// - `/users/*/update` matches `/users/{id}/update`
9+
/// - `/another/wildcard/*` matches `/another/wildcard/path`
10+
/// - `/another/wildcard/**` matches `/another/wildcard/long/path`
11+
/// - `path/**/parts` matches `path/with/several/parts`
12+
bool matchesPathPattern(String path, List<String> patterns) {
13+
const doubleStarStart = '__DOUBLE_STAR_START__';
14+
const doubleStarEnd = '__DOUBLE_STAR_END__';
15+
for (final pattern in patterns) {
16+
final processedPattern = pattern
17+
// Replace /** with end marker (to distinguish from single star)
18+
.replaceAll(
19+
'/**',
20+
doubleStarEnd,
21+
)
22+
// Replace ** with start marker (to distinguish from single star)
23+
.replaceAll(
24+
'**',
25+
doubleStarStart,
26+
);
27+
28+
// Escape all regex special characters
29+
var escapedPattern = RegExp.escape(
30+
processedPattern,
31+
)
32+
// Replace end marker of double star with .*
33+
.replaceAll(
34+
doubleStarEnd,
35+
'.*',
36+
)
37+
// Replace with optional trailing slash with any (0+) characters
38+
.replaceAll(
39+
doubleStarStart,
40+
'(?:/.*)?',
41+
)
42+
// Replace * with one or more characters except /
43+
.replaceAll(
44+
r'\*',
45+
'([^/]+)',
46+
);
47+
48+
// Special handling for patterns starting with * (but not **)
49+
// If pattern starts with *, but not with **, and path starts with /, add / at the beginning
50+
if (pattern.startsWith('*') &&
51+
!pattern.startsWith('**') &&
52+
path.startsWith('/')) {
53+
escapedPattern = '/$escapedPattern';
54+
}
55+
56+
final regex = RegExp('^$escapedPattern\$');
57+
if (regex.hasMatch(path)) {
58+
return true;
59+
}
60+
}
61+
return false;
62+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import 'package:swagger_parser/src/parser/utils/path_match.dart';
2+
import 'package:test/test.dart';
3+
4+
void main() {
5+
group('matchesPathPattern', () {
6+
test('exact match without wildcards', () {
7+
expect(matchesPathPattern('/api/users', ['/api/users']), isTrue);
8+
expect(matchesPathPattern('/api/users/profile', ['/api/users/profile']), isTrue);
9+
expect(matchesPathPattern('/exact/path/{id}', ['/exact/path/{id}']), isTrue);
10+
});
11+
12+
test('no match for different paths', () {
13+
expect(matchesPathPattern('/api/users', ['/api/posts']), isFalse);
14+
expect(matchesPathPattern('/api/users/profile', ['/api/users/settings']), isFalse);
15+
});
16+
17+
test('single asterisk wildcard (*)', () {
18+
// Basic single segment match
19+
expect(matchesPathPattern('/users/123/update', ['/users/*/update']), isTrue);
20+
expect(matchesPathPattern('/api/v1/users', ['/api/*/users']), isTrue);
21+
22+
// No match when path has more segments
23+
expect(matchesPathPattern('/users/123/profile/update', ['/users/*/update']), isFalse);
24+
expect(matchesPathPattern('/api/v1/extra/users', ['/api/*/users']), isFalse);
25+
26+
// Match at different positions
27+
expect(matchesPathPattern('/users/123', ['/users/*']), isTrue);
28+
expect(matchesPathPattern('/api/123/endpoint', ['/api/*/endpoint']), isTrue);
29+
});
30+
31+
test('double asterisk wildcard (**) - long path', () {
32+
expect(matchesPathPattern('/api/service/loyalty/balance/history', ['/api/service/loyalty/**']), isTrue);
33+
});
34+
35+
test('double asterisk wildcard (**) - another', () {
36+
expect(matchesPathPattern('/another/wildcard/long/path', ['/another/wildcard/**']), isTrue);
37+
});
38+
39+
test('double asterisk wildcard (**) - parts', () {
40+
expect(matchesPathPattern('path/with/several/parts', ['path/**/parts']), isTrue);
41+
});
42+
43+
test('double asterisk wildcard (**) - single', () {
44+
expect(matchesPathPattern('/api/single', ['/api/**']), isTrue);
45+
});
46+
47+
test('double asterisk wildcard (**) - wildcard', () {
48+
expect(matchesPathPattern('wildcard/path', ['wildcard/**']), isTrue);
49+
});
50+
51+
test('double asterisk wildcard (**) - empty', () {
52+
expect(matchesPathPattern('/api', ['/api/**']), isTrue);
53+
});
54+
55+
test('multiple patterns', () {
56+
expect(matchesPathPattern('/users/123/update', ['/posts/*/create', '/users/*/update']), isTrue);
57+
expect(matchesPathPattern('/api/v1/users', ['/api/v2/users', '/api/v1/*']), isTrue);
58+
expect(matchesPathPattern('/api/service/loyalty/balance', ['/api/*/loyalty', '/api/service/**']), isTrue);
59+
});
60+
61+
test('special regex characters in paths', () {
62+
// Dots in paths
63+
expect(matchesPathPattern('/api/v1.0/users', ['/api/*/users']), isTrue);
64+
expect(matchesPathPattern('/files/document.pdf', ['/files/*']), isTrue);
65+
66+
// Plus signs
67+
expect(matchesPathPattern('/api/v1+users', ['/api/*']), isTrue);
68+
69+
// Parentheses
70+
expect(matchesPathPattern('/api(v1)', ['/*']), isTrue);
71+
});
72+
73+
test('empty and edge cases', () {
74+
// Empty patterns list should not match
75+
expect(matchesPathPattern('/any/path', []), isFalse);
76+
77+
// Empty path
78+
expect(matchesPathPattern('', ['']), isTrue);
79+
expect(matchesPathPattern('', ['*']), isFalse); // * requires at least one non-slash char
80+
expect(matchesPathPattern('', ['**']), isTrue);
81+
82+
// Root path
83+
expect(matchesPathPattern('/', ['/']), isTrue);
84+
expect(matchesPathPattern('/', ['/*']), isFalse);
85+
});
86+
87+
test('complex patterns', () {
88+
// Mixed wildcards
89+
expect(matchesPathPattern('/api/v1/users/123/posts/456/comments', ['/api/*/users/*/posts/**']), isTrue);
90+
expect(matchesPathPattern('/users/123/profile/settings/advanced', ['/users/*/profile/**']), isTrue);
91+
92+
// Wildcard at start (matches one segment)
93+
expect(matchesPathPattern('/dynamic/endpoint', ['*/endpoint']), isTrue);
94+
expect(matchesPathPattern('/some/deep/path/endpoint', ['**/endpoint']), isTrue);
95+
});
96+
97+
test('no match cases', () {
98+
// Different structure
99+
expect(matchesPathPattern('/users/123/update', ['/posts/*/create']), isFalse);
100+
expect(matchesPathPattern('/api/v1/users', ['/api/v2/*']), isFalse);
101+
102+
// Too many segments for single *
103+
expect(matchesPathPattern('/api/deep/nested/path', ['/api/*']), isFalse);
104+
expect(matchesPathPattern('/users/123/profile/update', ['/users/*/update']), isFalse);
105+
106+
// Insufficient segments for **
107+
expect(matchesPathPattern('/api', ['/api/**/endpoint']), isFalse);
108+
});
109+
});
110+
}

0 commit comments

Comments
 (0)