Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support custom org name #148

Merged
merged 6 commits into from Jul 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
52 changes: 45 additions & 7 deletions lib/src/commands/create.dart
Expand Up @@ -12,10 +12,14 @@ import 'package:very_good_cli/src/command_runner.dart';
import 'package:very_good_cli/src/flutter_cli.dart';
import 'package:very_good_cli/src/templates/very_good_core_bundle.dart';

const _defaultOrgName = 'com.example.verygoodcore';

// A valid Dart identifier that can be used for a package, i.e. no
// capital letters.
// https://dart.dev/guides/language/language-tour#important-concepts
final RegExp _identifierRegExp = RegExp('[a-z_][a-z0-9_]*');
final RegExp _orgNameRegExp =
RegExp(r'[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+');

/// A method which returns a [Future<MasonGenerator>] given a [MasonBundle].
typedef GeneratorBuilder = Future<MasonGenerator> Function(MasonBundle);
Expand All @@ -32,12 +36,18 @@ class CreateCommand extends Command<int> {
}) : _analytics = analytics,
_logger = logger ?? Logger(),
_generator = generator ?? MasonGenerator.fromBundle {
argParser.addOption(
'project-name',
help: 'The project name for this new Flutter project. '
'This must be a valid dart package name.',
defaultsTo: null,
);
argParser
..addOption(
'project-name',
help: 'The project name for this new Flutter project. '
'This must be a valid dart package name.',
defaultsTo: null,
)
..addOption(
'org-name',
help: 'The organization for this new Flutter project.',
defaultsTo: 'com.example.verygoodcore',
);
}

final Analytics _analytics;
Expand Down Expand Up @@ -67,11 +77,12 @@ class CreateCommand extends Command<int> {
Future<int> run() async {
final outputDirectory = _outputDirectory;
final projectName = _projectName;
final orgName = _orgName;
final generateDone = _logger.progress('Bootstrapping');
final generator = await _generator(veryGoodCoreBundle);
final fileCount = await generator.generate(
DirectoryGeneratorTarget(outputDirectory, _logger),
vars: {'project_name': projectName},
vars: {'project_name': projectName, 'org_name': orgName},
);
generateDone('Generated $fileCount file(s)');

Expand Down Expand Up @@ -126,6 +137,28 @@ class CreateCommand extends Command<int> {
return projectName;
}

/// Gets the organization name.
List<String> get _orgName {
if (_argResults['org-name'] == null) return _defaultOrgName.split('.');

final orgName = _argResults['org-name'] as String;
_validateOrgName(orgName);
return orgName.split('.');
}

void _validateOrgName(String name) {
final isValidOrgName = _isValidOrgName(name);
if (!isValidOrgName) {
throw UsageException(
'"$name" is not a valid org name.\n\n'
'A valid org name has 3 parts separated by "."'
'and only includes alphanumeric characters and underscores'
'(ex. very.good.org)',
usage,
);
}
}

void _validateProjectName(String name) {
final isValidProjectName = _isValidPackageName(name);
if (!isValidProjectName) {
Expand All @@ -137,6 +170,11 @@ class CreateCommand extends Command<int> {
}
}

bool _isValidOrgName(String name) {
final match = _orgNameRegExp.matchAsPrefix(name);
return match != null && match.end == name.length;
}

bool _isValidPackageName(String name) {
final match = _identifierRegExp.matchAsPrefix(name);
return match != null && match.end == name.length;
Expand Down
101 changes: 100 additions & 1 deletion test/src/commands/create_test.dart
Expand Up @@ -126,7 +126,10 @@ void main() {
'.tmp',
),
),
vars: {'project_name': 'my_app'},
vars: {
'project_name': 'my_app',
'org_name': ['com', 'example', 'verygoodcore'],
},
),
).called(1);
verify(
Expand All @@ -140,5 +143,101 @@ void main() {
() => analytics.waitForLastPing(timeout: VeryGoodCommandRunner.timeout),
).called(1);
});

group('org-name', () {
group('invalid --org-name', () {
test('no delimiters', () async {
const expectedErrorMessage = '"My App" is not a valid org name.\n\n'
'A valid org name has 3 parts separated by "."'
'and only includes alphanumeric characters and underscores'
'(ex. very.good.org)';
final result = await commandRunner.run(
['create', '.', '--org-name', 'My App'],
);
expect(result, equals(ExitCode.usage.code));
verify(() => logger.err(expectedErrorMessage)).called(1);
});

test('more than 3 domains', () async {
const expectedErrorMessage =
'"very.bad.test.case" is not a valid org name.\n\n'
'A valid org name has 3 parts separated by "."'
'and only includes alphanumeric characters and underscores'
'(ex. very.good.org)';
final result = await commandRunner.run(
['create', '.', '--org-name', 'very.bad.test.case'],
);
expect(result, equals(ExitCode.usage.code));
verify(() => logger.err(expectedErrorMessage)).called(1);
});

test('invalid characters present', () async {
const expectedErrorMessage =
'"very%.bad@.#test" is not a valid org name.\n\n'
'A valid org name has 3 parts separated by "."'
'and only includes alphanumeric characters and underscores'
'(ex. very.good.org)';
final result = await commandRunner.run(
['create', '.', '--org-name', 'very%.bad@.#test'],
);
expect(result, equals(ExitCode.usage.code));
verify(() => logger.err(expectedErrorMessage)).called(1);
});
});

group('valid --org-name', () {
test('completes successfully with correct output', () async {
final argResults = MockArgResults();
final generator = MockMasonGenerator();
final command = CreateCommand(
analytics: analytics,
logger: logger,
generator: (_) async => generator,
)..argResultOverrides = argResults;
when(() => argResults['project-name']).thenReturn('my_app');
when(() => argResults['org-name']).thenReturn('very.good.ventures');
when(() => argResults.rest).thenReturn(['.tmp']);
when(() => generator.id).thenReturn('generator_id');
when(() => generator.description).thenReturn('generator description');
when(
() => generator.generate(any(), vars: any(named: 'vars')),
).thenAnswer((_) async => 62);
final result = await command.run();
expect(result, equals(ExitCode.success.code));
verify(() => logger.progress('Bootstrapping')).called(1);
expect(progressLogs, equals(['Generated 62 file(s)']));
verify(
() => logger.progress('Running "flutter packages get" in .tmp'),
).called(1);
verify(() => logger.alert('Created a Very Good App! 🦄')).called(1);
verify(
() => generator.generate(
any(
that: isA<DirectoryGeneratorTarget>().having(
(g) => g.dir.path,
'dir',
'.tmp',
),
),
vars: {
'project_name': 'my_app',
'org_name': ['very', 'good', 'ventures'],
},
),
).called(1);
verify(
() => analytics.sendEvent(
'create',
'generator_id',
label: 'generator description',
),
).called(1);
verify(
() => analytics.waitForLastPing(
timeout: VeryGoodCommandRunner.timeout),
).called(1);
});
});
});
});
}