Skip to content

Commit 02437ce

Browse files
erickzanardorenancaraujoalestiago
authored
feat: adding list command (#669)
* feat: adding list command * Apply suggestions from code review Co-authored-by: Alejandro Santiago <[email protected]> * feat: pr suggestions --------- Co-authored-by: Renan <[email protected]> Co-authored-by: Alejandro Santiago <[email protected]>
1 parent 3f15b30 commit 02437ce

File tree

8 files changed

+272
-1
lines changed

8 files changed

+272
-1
lines changed

packages/dart_frog_cli/.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@
66
pubspec.lock
77

88
# Test related files
9-
coverage/
9+
coverage/
10+
11+
build

packages/dart_frog_cli/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Available commands:
2727
build Create a production build.
2828
create Creates a new Dart Frog app.
2929
dev Run a local development server.
30+
list Lists all the routes on a Dart Frog project.
3031
new Create a new route or middleware for dart_frog.
3132
update Update the Dart Frog CLI.
3233

packages/dart_frog_cli/lib/src/command_runner.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class DartFrogCommandRunner extends CompletionCommandRunner<int> {
4343
addCommand(DevCommand(logger: _logger));
4444
addCommand(UpdateCommand(logger: _logger));
4545
addCommand(NewCommand(logger: _logger));
46+
addCommand(ListCommand(logger: _logger));
4647
}
4748

4849
final Logger _logger;

packages/dart_frog_cli/lib/src/commands/commands.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:mason/mason.dart';
33
export 'build/build.dart';
44
export 'create/create.dart';
55
export 'dev/dev.dart';
6+
export 'list/list.dart';
67
export 'new/new.dart';
78

89
/// A method which returns a [Future<MasonGenerator>] given a [MasonBundle].
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import 'dart:io';
2+
3+
import 'package:args/command_runner.dart';
4+
import 'package:dart_frog_cli/src/command.dart';
5+
import 'package:dart_frog_gen/dart_frog_gen.dart';
6+
import 'package:mason/mason.dart';
7+
8+
/// Definition for a function that builds a [RouteConfiguration] from a
9+
/// [Directory].
10+
typedef RouteConfigurationBuilder = RouteConfiguration Function(
11+
Directory directory,
12+
);
13+
14+
/// {@template list_command}
15+
/// `dart_frog list "path/to/project"`
16+
///
17+
/// Lists the routes on the project.
18+
/// {@endtemplate}
19+
class ListCommand extends DartFrogCommand {
20+
/// {@macro list_command}
21+
ListCommand({
22+
super.logger,
23+
RouteConfigurationBuilder buildConfiguration = buildRouteConfiguration,
24+
}) : _buildConfiguration = buildConfiguration {
25+
argParser.addFlag(
26+
'plain',
27+
abbr: 'p',
28+
help: 'Return the output in a plain format, printing each route on a new '
29+
'line.',
30+
negatable: false,
31+
);
32+
}
33+
34+
final RouteConfigurationBuilder _buildConfiguration;
35+
36+
@override
37+
String get description => 'Lists the routes on a Dart Frog project.';
38+
39+
@override
40+
String get name => 'list';
41+
42+
@override
43+
final String invocation = 'dart_frog list "path/to/project"';
44+
45+
@override
46+
Future<int> run() async {
47+
final projectDir = _projectDirectory;
48+
49+
final configuration = _buildConfiguration(projectDir);
50+
51+
final plain = results['plain'] as bool;
52+
53+
if (!plain) {
54+
logger
55+
..info('Route list 🐸:')
56+
..info('==============\n');
57+
}
58+
59+
for (final endpoint in configuration.endpoints.keys) {
60+
logger.info(endpoint);
61+
}
62+
63+
return ExitCode.success.code;
64+
}
65+
66+
Directory get _projectDirectory {
67+
final rest = results.rest;
68+
_validateProjectDirectoryArg(rest);
69+
return Directory(
70+
rest.isEmpty ? Directory.current.path : rest.first,
71+
);
72+
}
73+
74+
void _validateProjectDirectoryArg(List<String> args) {
75+
if (args.length > 1) {
76+
throw UsageException(
77+
'Multiple project directories specified.',
78+
usageString,
79+
);
80+
}
81+
}
82+
}

packages/dart_frog_cli/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ environment:
1212
dependencies:
1313
args: ^2.1.0
1414
cli_completion: ^0.3.0
15+
dart_frog_gen: ^0.4.0
1516
mason: ^0.1.0-dev.39
1617
meta: ^1.7.0
1718
path: ^1.8.1

packages/dart_frog_cli/test/src/command_runner_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const expectedUsage = [
3131
' build Create a production build.\n'
3232
' create Creates a new Dart Frog app.\n'
3333
' dev Run a local development server.\n'
34+
' list Lists the routes on a Dart Frog project.\n'
3435
' new Create a new route or middleware for dart_frog\n'
3536
' update Update the Dart Frog CLI.\n'
3637
'\n'
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// ignore_for_file: no_adjacent_strings_in_list
2+
import 'dart:io';
3+
4+
import 'package:args/args.dart';
5+
import 'package:args/command_runner.dart';
6+
import 'package:dart_frog_cli/src/command_runner.dart';
7+
import 'package:dart_frog_cli/src/commands/commands.dart';
8+
import 'package:dart_frog_gen/dart_frog_gen.dart';
9+
import 'package:mason/mason.dart';
10+
import 'package:mocktail/mocktail.dart';
11+
import 'package:pub_updater/pub_updater.dart';
12+
import 'package:test/test.dart';
13+
14+
import '../../../helpers/helpers.dart';
15+
16+
class _MockArgResults extends Mock implements ArgResults {}
17+
18+
class _MockLogger extends Mock implements Logger {}
19+
20+
class _MockPubUpdater extends Mock implements PubUpdater {}
21+
22+
class _MockProcessSignal extends Mock implements ProcessSignal {}
23+
24+
class _MockProgress extends Mock implements Progress {}
25+
26+
class _FakeDirectoryGeneratorTarget extends Fake
27+
implements DirectoryGeneratorTarget {}
28+
29+
class _MockRouteConfiguration extends Mock implements RouteConfiguration {}
30+
31+
const expectedUsage = [
32+
'Lists the routes on a Dart Frog project.\n'
33+
'\n'
34+
'Usage: dart_frog list "path/to/project"\n'
35+
'-h, --help Print this usage information.\n'
36+
'-p, --plain Return the output in a plain format, printing each route '
37+
'on a new line.\n'
38+
'\n'
39+
'Run "dart_frog help" to see global options.'
40+
];
41+
42+
void main() {
43+
group('dart_frog list', () {
44+
setUpAll(() {
45+
registerFallbackValue(_FakeDirectoryGeneratorTarget());
46+
});
47+
48+
late ArgResults argResults;
49+
late Logger logger;
50+
late Progress progress;
51+
late ListCommand command;
52+
late DartFrogCommandRunner commandRunner;
53+
late RouteConfiguration routeConfiguration;
54+
55+
setUp(() {
56+
argResults = _MockArgResults();
57+
logger = _MockLogger();
58+
progress = _MockProgress();
59+
routeConfiguration = _MockRouteConfiguration();
60+
when(() => logger.progress(any())).thenReturn(progress);
61+
command = ListCommand(
62+
logger: logger,
63+
buildConfiguration: (_) => routeConfiguration,
64+
)
65+
..testArgResults = argResults
66+
..testUsage = 'test usage';
67+
68+
when(() => argResults['plain']).thenReturn(false);
69+
70+
final sigint = _MockProcessSignal();
71+
72+
when(sigint.watch).thenAnswer((_) => const Stream.empty());
73+
74+
commandRunner = DartFrogCommandRunner(
75+
logger: logger,
76+
pubUpdater: _MockPubUpdater(),
77+
exit: (_) {},
78+
sigint: sigint,
79+
);
80+
});
81+
82+
test(
83+
'usage shows help text',
84+
overridePrint((printLogs) async {
85+
final result = await commandRunner.run(['list', '--help']);
86+
87+
expect(result, equals(ExitCode.success.code));
88+
expect(printLogs, expectedUsage);
89+
}),
90+
);
91+
92+
test('logs all the endpoints', () async {
93+
when(() => routeConfiguration.endpoints).thenReturn({
94+
'/turles/<id>': [],
95+
'/turles/random': [],
96+
});
97+
final directory = Directory.systemTemp.createTempSync();
98+
99+
command.testCwd = directory;
100+
101+
when(() => argResults.rest).thenReturn(['my_project']);
102+
103+
await expectLater(
104+
await command.run(),
105+
equals(ExitCode.success.code),
106+
);
107+
108+
verify(() => logger.info('Route list 🐸:')).called(1);
109+
verify(() => logger.info('==============\n')).called(1);
110+
verify(() => logger.info('/turles/<id>')).called(1);
111+
verify(() => logger.info('/turles/random')).called(1);
112+
});
113+
114+
test('logs all the endpoints in plain mode', () async {
115+
when(() => argResults['plain']).thenReturn(true);
116+
117+
when(() => routeConfiguration.endpoints).thenReturn({
118+
'/turles/<id>': [],
119+
'/turles/random': [],
120+
});
121+
final directory = Directory.systemTemp.createTempSync();
122+
123+
command.testCwd = directory;
124+
125+
when(() => argResults.rest).thenReturn(['my_project']);
126+
127+
await expectLater(
128+
await command.run(),
129+
equals(ExitCode.success.code),
130+
);
131+
132+
verifyNever(() => logger.info('Route list 🐸:'));
133+
verifyNever(() => logger.info('==============\n'));
134+
verify(() => logger.info('/turles/<id>')).called(1);
135+
verify(() => logger.info('/turles/random')).called(1);
136+
});
137+
138+
test(
139+
'logs all the endpoints of the current dir when a project is ommited',
140+
() async {
141+
when(() => routeConfiguration.endpoints).thenReturn({
142+
'/turles/<id>': [],
143+
'/turles/random': [],
144+
});
145+
final directory = Directory.systemTemp.createTempSync();
146+
147+
command.testCwd = directory;
148+
149+
when(() => argResults.rest).thenReturn([]);
150+
151+
await expectLater(
152+
await command.run(),
153+
equals(ExitCode.success.code),
154+
);
155+
156+
verify(() => logger.info('Route list 🐸:')).called(1);
157+
verify(() => logger.info('==============\n')).called(1);
158+
verify(() => logger.info('/turles/<id>')).called(1);
159+
verify(() => logger.info('/turles/random')).called(1);
160+
},
161+
);
162+
163+
test('fails when multiple directories are specified', () async {
164+
final directory = Directory.systemTemp.createTempSync();
165+
166+
command.testCwd = directory;
167+
168+
when(() => argResults.rest).thenReturn(['a', 'b']);
169+
170+
await expectLater(
171+
() async => command.run(),
172+
throwsA(
173+
isA<UsageException>().having(
174+
(p) => p.message,
175+
'error message',
176+
'Multiple project directories specified.',
177+
),
178+
),
179+
);
180+
});
181+
});
182+
}

0 commit comments

Comments
 (0)