Skip to content

Commit c0a2970

Browse files
authored
feat: support custom org name (#148)
1 parent 8fa668c commit c0a2970

File tree

2 files changed

+145
-8
lines changed

2 files changed

+145
-8
lines changed

lib/src/commands/create.dart

+45-7
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ import 'package:very_good_cli/src/command_runner.dart';
1212
import 'package:very_good_cli/src/flutter_cli.dart';
1313
import 'package:very_good_cli/src/templates/very_good_core_bundle.dart';
1414

15+
const _defaultOrgName = 'com.example.verygoodcore';
16+
1517
// A valid Dart identifier that can be used for a package, i.e. no
1618
// capital letters.
1719
// https://dart.dev/guides/language/language-tour#important-concepts
1820
final RegExp _identifierRegExp = RegExp('[a-z_][a-z0-9_]*');
21+
final RegExp _orgNameRegExp =
22+
RegExp(r'[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+');
1923

2024
/// A method which returns a [Future<MasonGenerator>] given a [MasonBundle].
2125
typedef GeneratorBuilder = Future<MasonGenerator> Function(MasonBundle);
@@ -32,12 +36,18 @@ class CreateCommand extends Command<int> {
3236
}) : _analytics = analytics,
3337
_logger = logger ?? Logger(),
3438
_generator = generator ?? MasonGenerator.fromBundle {
35-
argParser.addOption(
36-
'project-name',
37-
help: 'The project name for this new Flutter project. '
38-
'This must be a valid dart package name.',
39-
defaultsTo: null,
40-
);
39+
argParser
40+
..addOption(
41+
'project-name',
42+
help: 'The project name for this new Flutter project. '
43+
'This must be a valid dart package name.',
44+
defaultsTo: null,
45+
)
46+
..addOption(
47+
'org-name',
48+
help: 'The organization for this new Flutter project.',
49+
defaultsTo: 'com.example.verygoodcore',
50+
);
4151
}
4252

4353
final Analytics _analytics;
@@ -67,11 +77,12 @@ class CreateCommand extends Command<int> {
6777
Future<int> run() async {
6878
final outputDirectory = _outputDirectory;
6979
final projectName = _projectName;
80+
final orgName = _orgName;
7081
final generateDone = _logger.progress('Bootstrapping');
7182
final generator = await _generator(veryGoodCoreBundle);
7283
final fileCount = await generator.generate(
7384
DirectoryGeneratorTarget(outputDirectory, _logger),
74-
vars: {'project_name': projectName},
85+
vars: {'project_name': projectName, 'org_name': orgName},
7586
);
7687
generateDone('Generated $fileCount file(s)');
7788

@@ -126,6 +137,28 @@ class CreateCommand extends Command<int> {
126137
return projectName;
127138
}
128139

140+
/// Gets the organization name.
141+
List<String> get _orgName {
142+
if (_argResults['org-name'] == null) return _defaultOrgName.split('.');
143+
144+
final orgName = _argResults['org-name'] as String;
145+
_validateOrgName(orgName);
146+
return orgName.split('.');
147+
}
148+
149+
void _validateOrgName(String name) {
150+
final isValidOrgName = _isValidOrgName(name);
151+
if (!isValidOrgName) {
152+
throw UsageException(
153+
'"$name" is not a valid org name.\n\n'
154+
'A valid org name has 3 parts separated by "."'
155+
'and only includes alphanumeric characters and underscores'
156+
'(ex. very.good.org)',
157+
usage,
158+
);
159+
}
160+
}
161+
129162
void _validateProjectName(String name) {
130163
final isValidProjectName = _isValidPackageName(name);
131164
if (!isValidProjectName) {
@@ -137,6 +170,11 @@ class CreateCommand extends Command<int> {
137170
}
138171
}
139172

173+
bool _isValidOrgName(String name) {
174+
final match = _orgNameRegExp.matchAsPrefix(name);
175+
return match != null && match.end == name.length;
176+
}
177+
140178
bool _isValidPackageName(String name) {
141179
final match = _identifierRegExp.matchAsPrefix(name);
142180
return match != null && match.end == name.length;

test/src/commands/create_test.dart

+100-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,10 @@ void main() {
126126
'.tmp',
127127
),
128128
),
129-
vars: {'project_name': 'my_app'},
129+
vars: {
130+
'project_name': 'my_app',
131+
'org_name': ['com', 'example', 'verygoodcore'],
132+
},
130133
),
131134
).called(1);
132135
verify(
@@ -140,5 +143,101 @@ void main() {
140143
() => analytics.waitForLastPing(timeout: VeryGoodCommandRunner.timeout),
141144
).called(1);
142145
});
146+
147+
group('org-name', () {
148+
group('invalid --org-name', () {
149+
test('no delimiters', () async {
150+
const expectedErrorMessage = '"My App" is not a valid org name.\n\n'
151+
'A valid org name has 3 parts separated by "."'
152+
'and only includes alphanumeric characters and underscores'
153+
'(ex. very.good.org)';
154+
final result = await commandRunner.run(
155+
['create', '.', '--org-name', 'My App'],
156+
);
157+
expect(result, equals(ExitCode.usage.code));
158+
verify(() => logger.err(expectedErrorMessage)).called(1);
159+
});
160+
161+
test('more than 3 domains', () async {
162+
const expectedErrorMessage =
163+
'"very.bad.test.case" is not a valid org name.\n\n'
164+
'A valid org name has 3 parts separated by "."'
165+
'and only includes alphanumeric characters and underscores'
166+
'(ex. very.good.org)';
167+
final result = await commandRunner.run(
168+
['create', '.', '--org-name', 'very.bad.test.case'],
169+
);
170+
expect(result, equals(ExitCode.usage.code));
171+
verify(() => logger.err(expectedErrorMessage)).called(1);
172+
});
173+
174+
test('invalid characters present', () async {
175+
const expectedErrorMessage =
176+
'"very%.bad@.#test" is not a valid org name.\n\n'
177+
'A valid org name has 3 parts separated by "."'
178+
'and only includes alphanumeric characters and underscores'
179+
'(ex. very.good.org)';
180+
final result = await commandRunner.run(
181+
['create', '.', '--org-name', 'very%.bad@.#test'],
182+
);
183+
expect(result, equals(ExitCode.usage.code));
184+
verify(() => logger.err(expectedErrorMessage)).called(1);
185+
});
186+
});
187+
188+
group('valid --org-name', () {
189+
test('completes successfully with correct output', () async {
190+
final argResults = MockArgResults();
191+
final generator = MockMasonGenerator();
192+
final command = CreateCommand(
193+
analytics: analytics,
194+
logger: logger,
195+
generator: (_) async => generator,
196+
)..argResultOverrides = argResults;
197+
when(() => argResults['project-name']).thenReturn('my_app');
198+
when(() => argResults['org-name']).thenReturn('very.good.ventures');
199+
when(() => argResults.rest).thenReturn(['.tmp']);
200+
when(() => generator.id).thenReturn('generator_id');
201+
when(() => generator.description).thenReturn('generator description');
202+
when(
203+
() => generator.generate(any(), vars: any(named: 'vars')),
204+
).thenAnswer((_) async => 62);
205+
final result = await command.run();
206+
expect(result, equals(ExitCode.success.code));
207+
verify(() => logger.progress('Bootstrapping')).called(1);
208+
expect(progressLogs, equals(['Generated 62 file(s)']));
209+
verify(
210+
() => logger.progress('Running "flutter packages get" in .tmp'),
211+
).called(1);
212+
verify(() => logger.alert('Created a Very Good App! 🦄')).called(1);
213+
verify(
214+
() => generator.generate(
215+
any(
216+
that: isA<DirectoryGeneratorTarget>().having(
217+
(g) => g.dir.path,
218+
'dir',
219+
'.tmp',
220+
),
221+
),
222+
vars: {
223+
'project_name': 'my_app',
224+
'org_name': ['very', 'good', 'ventures'],
225+
},
226+
),
227+
).called(1);
228+
verify(
229+
() => analytics.sendEvent(
230+
'create',
231+
'generator_id',
232+
label: 'generator description',
233+
),
234+
).called(1);
235+
verify(
236+
() => analytics.waitForLastPing(
237+
timeout: VeryGoodCommandRunner.timeout),
238+
).called(1);
239+
});
240+
});
241+
});
143242
});
144243
}

0 commit comments

Comments
 (0)