Idgen dart

This commit is contained in:
Tanner Sommers 2024-10-19 14:31:02 -05:00
parent 12a701e1c5
commit 3745b39726
12 changed files with 676 additions and 24 deletions

29
.gitignore vendored
View File

@ -1,27 +1,8 @@
# See https://www.dartlang.org/guides/libraries/private-files
# Files and directories created by pub
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
.packages
build/
# If you're building an application, you may want to check-in your pubspec.lock
# Avoid committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
# dotenv environment variables file
.env*
# Avoid committing generated Javascript files:
*.dart.js
*.info.json # Produced by the --dump-info flag.
*.js # When generated by dart2js. Don't specify *.js if your
# project includes source files written in JavaScript.
*.js_
*.js.deps
*.js.map
.flutter-plugins
.flutter-plugins-dependencies

3
CHANGELOG.md Normal file
View File

@ -0,0 +1,3 @@
## 1.0.0
- Initial version.

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) [2024] [SticksDev]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

27
README.md Normal file
View File

@ -0,0 +1,27 @@
This package allows you to generate unique and random IDs for your Flutter/dart applications using the ID generator provided
by [team hydra](https://teamhydra.dev/).
## Features
This generator provides the following ID types:
- UUIDv4 generation
- NanoID generation (with and without custom alphabet)
- Word generation
- 2FA generation
- Keypair generation
- Snowflake generation (discord-like snowflake)
- License key generation
## Getting started
You will need a valid username and token to use this package. If you do not have one and would like to use this package, please contact us on our [discord server](https://discord.gg/zira) in the `#other-support` channel and someone will assist you.
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
```dart
const like = 'sample';
```

1
analysis_options.yaml Normal file
View File

@ -0,0 +1 @@
include: package:lints/recommended.yaml

View File

@ -0,0 +1,11 @@
// Import the package
import 'package:teamhydra_idgen/teamhydra_idgen.dart';
void main() async {
IDGen idgen = IDGen(username: 'your_username', token: 'your_token');
// Generate a new ID, in this example we are generating a UUID V4
IDGenResponse uuid =
await idgen.generateUUIDV4(); // all generations are async
print('Generated UUID: ${uuid.id}'); // print the generated ID
}

View File

@ -0,0 +1,40 @@
/// Response class for IDs generated by IDGen.
class IDGenResponse {
final String id;
IDGenResponse({required this.id});
factory IDGenResponse.fromJson(Map<String, dynamic> json) {
return IDGenResponse(
id: json['id'],
);
}
}
/// Response class for keypairs generated by IDGen.
class IDKeypairResponse implements IDGenResponse {
@override
final String id;
final String privateID;
IDKeypairResponse({required this.id, required this.privateID});
factory IDKeypairResponse.fromJson(Map<String, dynamic> json) {
return IDKeypairResponse(
id: json['id'],
privateID: json['privateID'],
);
}
}
/// Custom exception for IDGen, thrown when an error occurs when generating an ID.
class IDGenException implements Exception {
final String message;
IDGenException(this.message);
@override
String toString() {
return message;
}
}

83
lib/src/idgen_http.dart Normal file
View File

@ -0,0 +1,83 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:teamhydra_idgen/src/idgen_dart_base.dart';
class IDGenHTTPWorker {
final Dio _dio = Dio();
final String _baseURL = 'https://id.hydra.workers.dev';
final String username;
final String token;
IDGenHTTPWorker({required this.username, required this.token});
Future<IDGenResponse> generate(
String type, Map<String, dynamic>? data) async {
try {
final response = await _dio.post(_baseURL,
data: {
'username': username,
'token': token,
'type': type,
...?data,
},
options: Options(contentType: Headers.jsonContentType));
return IDGenResponse.fromJson(response.data);
} on DioException catch (e) {
Map<String, dynamic>? errorData;
// Do we have a response?
if (e.response != null) {
// Try decoding the response
try {
errorData = jsonDecode(e.response!.data);
} catch (_) {
// Do nothing
}
}
if (errorData != null && errorData['error'] != null) {
throw IDGenException(
'Server rejected ID generation: ${errorData['error']}');
} else if (errorData != null) {
throw IDGenException('Server rejected ID generation: $errorData');
} else {
throw IDGenException(
'An error occurred during generation ($type): ${e.message}');
}
} catch (e) {
throw IDGenException(
'An unknown error occurred during generation ($type): $e');
}
}
Future<IDKeypairResponse> generateKeypair() async {
try {
final response = await _dio.post(_baseURL,
data: {
'username': username,
'token': token,
'type': 'keypair',
},
options: Options(contentType: Headers.jsonContentType));
return IDKeypairResponse.fromJson(response.data);
} on DioException catch (e) {
final errorData = e.response?.data;
if (errorData != null && errorData['error'] != null) {
throw IDGenException(
'Server rejected ID generation: ${errorData['error']}');
} else if (errorData != null) {
throw IDGenException('Server rejected ID generation: $errorData');
} else {
throw IDGenException(
'An error occurred during generation (keypair): ${e.message}');
}
} catch (e) {
throw IDGenException(
'An unknown error occurred during generation (keypair): $e');
}
}
}

145
lib/src/idgen_main.dart Normal file
View File

@ -0,0 +1,145 @@
import 'package:teamhydra_idgen/src/idgen_http.dart';
import 'package:teamhydra_idgen/teamhydra_idgen.dart';
/// The main class to use when generating an ID
///
/// All returned IDs are in the form of a [IDGenResponse] object which contains the ID.
/// If an error occurs, an [IDGenException] is thrown.
class IDGen {
final String username;
final String token;
final IDGenHTTPWorker _worker;
/// Create a new IDGen class to generate IDs
///
/// [username] and [token] are required to authenticate with the IDGen API.
IDGen({required this.username, required this.token})
: _worker = IDGenHTTPWorker(username: username, token: token);
/// Generate a new UUID V4
///
/// Returns a [IDGenResponse] object containing the generated ID.
/// Throws an [IDGenException] if an error occurs.
Future<IDGenResponse> generateUUIDV4() async {
return await _worker.generate('uuid', null);
}
/// Generate a new nanoID
///
/// Returns a [IDGenResponse] object containing the generated ID.
/// Optionally, you can pass a [size] parameter to specify the length of the nanoID, default is 10.
/// You can also pass a [alphabet] parameter to specify the characters used in the nanoID, default is provided by the API.
///
/// Throws an [IDGenException] if an error occurs.
///
/// Throws an [ArgumentError] if the size is not between 1 and 256 or the alphabet is empty.
Future<IDGenResponse> generateNanoID({int? size, String? alphabet}) async {
// Ensure length is between 1 and 256 (if specified)
if (size != null && (size < 1 || size > 256)) {
throw ArgumentError(
'Cannot generate a nanoID with a length of $size, must be between 1 and 256');
}
// Ensure alphabet is not empty (if specified)
if (alphabet != null && alphabet.isEmpty) {
throw ArgumentError('Cannot generate a nanoID with an empty alphabet');
}
// Ensure alphabet is at least 3 characters long and not longer than 256
if (alphabet != null && (alphabet.length < 3 || alphabet.length > 256)) {
throw ArgumentError(
'Cannot generate a nanoID with an alphabet of length ${alphabet.length}, must be between 3 and 256');
}
return await _worker.generate('nanoid', {
if (size != null) 'length': size else 'length': 10,
if (alphabet != null) 'alphabet': alphabet,
});
}
/// Generate a 2FA code pair
///
/// Returns a [IDGenResponse] object containing the generated ID.
///
/// Optionally, you can pass a [length] parameter to specify the length of the 2FA code, default is 6.
///
/// Throws an [IDGenException] if an error occurs.
///
/// Throws an [ArgumentError] if the length is not between 1 and 256.
Future<IDGenResponse> generate2FACode({int? length}) async {
// Ensure length is between 1 and 256 (if specified)
if (length != null && (length < 1 || length > 256)) {
throw ArgumentError(
'Cannot generate a 2FA code with a length of $length, must be between 1 and 256');
}
return await _worker.generate('2fa', {
if (length != null) 'length': length,
});
}
/// Generate a license key
///
/// Returns a [IDGenResponse] object containing the generated ID.
/// Keys are generated with a 25 character length, resulting in a 5-5-5-5-5 format.
///
/// Throws an [IDGenException] if an error occurs.
Future<IDGenResponse> generateLicenseKey() async {
return await _worker.generate('license', null);
}
/// Generate word based string
///
/// Returns a [IDGenResponse] object containing the generated ID.
/// Optionally, you can pass a [length] parameter to specify the length of the word based string, default is 5.
/// You can also pass a [separator] parameter to specify the separator used in the word based string, options are 'slug', 'title' and 'formal'. Default is 'slug'.
///
/// Slug: lowercase words separated by hyphens
///
/// Title: Title case words separated by spaces
///
/// Formal: Title case words with no spaces or separators
///
/// Throws an [IDGenException] if an error occurs.
///
/// Throws an [ArgumentError] if the length is not between 1 and 16 or the separator is invalid.
Future<IDGenResponse> generateWordBasedString(
{int? length, String? separator}) async {
// Ensure length is between 1 and 256 (if specified)
if (length != null && (length < 1 || length > 16)) {
throw ArgumentError(
'Cannot generate a word based string with a length of $length, must be between 1 and 16');
}
// Ensure separator is valid (if specified)
if (separator != null &&
separator != 'slug' &&
separator != 'title' &&
separator != 'formal') {
throw ArgumentError(
'Cannot generate a word based string with an invalid separator');
}
return await _worker.generate('word', {
if (length != null) 'length': length else 'length': 5,
if (separator != null) 'style': separator else 'style': 'slug',
});
}
/// Generate a snowflake ID
///
/// Returns a [IDKeypairResponse] object containing
///
/// Throws an [IDGenException] if an error occurs.
Future<IDGenResponse> generateSnowflakeID() async {
return await _worker.generate('snowflake', null);
}
/// Generate a keypair
///
/// Returns a [IDKeypairResponse] object containing the generated ID and secret key.
/// Throws an [IDGenException] if an error occurs.
Future<IDKeypairResponse> generateKeypair() async {
return await _worker.generateKeypair();
}
}

5
lib/teamhydra_idgen.dart Normal file
View File

@ -0,0 +1,5 @@
/// Team Hydra ID Generator Library - Generate unique IDs for your projects, using the IDGen API.
library;
export 'src/idgen_dart_base.dart';
export 'src/idgen_main.dart';

17
pubspec.yaml Normal file
View File

@ -0,0 +1,17 @@
name: teamhydra_idgen
description: A library for generating unique IDs using Team Hydra's ID generation service.
version: 1.0.0
repository: https://teamhydra.io/Sticks/idgen-dart
environment:
sdk: ^3.5.4
# Add regular dependencies here.
dependencies:
dio: ^5.7.0
# path: ^1.8.0
dev_dependencies:
lints: ^4.0.0
test: ^1.24.0
dotenv: ^4.2.0

318
test/idgen_dart_test.dart Normal file
View File

@ -0,0 +1,318 @@
import 'dart:io';
import 'package:teamhydra_idgen/teamhydra_idgen.dart';
import 'package:test/test.dart';
Map<String, String> loadRequiredVarsFromEnv() {
// Load the .env file from the current directory
final currentDir = Directory.current;
final envFile = File('${currentDir.path}/.env');
if (!envFile.existsSync()) {
throw Exception(
'Could not locate the .env file in the current directory (tried path: ${envFile.path})');
}
final lines = envFile
.readAsLinesSync()
.where((line) => line.isNotEmpty && !line.startsWith('#'))
.toList(); // Filter out empty lines and comments
String username = '';
String token = '';
// Get the username and token (IDGEN_USERNAME and IDGEN_TOKEN)
for (final line in lines) {
if (line.startsWith('IDGEN_USERNAME=')) {
username = line.split('=')[1].trim();
} else if (line.startsWith('IDGEN_TOKEN=')) {
token = line.split('=')[1].trim();
}
}
// Remove " from the strings
username = username.replaceAll('"', '');
token = token.replaceAll('"', '');
if (username.isEmpty || token.isEmpty) {
throw Exception(
'IDGEN_USERNAME or IDGEN_TOKEN is missing from the .env file');
}
return {'username': username, 'token': token};
}
void main() {
// Load the .env file and get the username and token
final env = loadRequiredVarsFromEnv();
if (env['username'] == null ||
env['token'] == null ||
env['username']!.isEmpty ||
env['token']!.isEmpty) {
print('Please provide a valid username and token in the .env file');
exit(1);
}
final username = env['username']!;
final token = env['token']!;
print(
"[IDGen] Loaded credentials for username: $username and token (last 4): ${token.substring(token.length - 4)} to run tests");
group('IDGen', () {
final idGen = IDGen(username: username, token: token);
// Ensure it errors when invalid user/token combo is used
test('Invalid user/token combo', () async {
final idGen = IDGen(username: 'invalid', token: 'invalid');
try {
await idGen.generateUUIDV4();
fail('Should have thrown an exception');
} on IDGenException catch (e) {
expect(e.message,
'Server rejected ID generation: Invalid username or token');
} catch (e) {
fail('Should have thrown an IDGenException');
}
});
// Ensure it generates a UUID V4
test('Generate UUID V4', () async {
final response = await idGen.generateUUIDV4();
expect(response.id, isNotNull);
expect(response.id, isNotEmpty);
expect(response.id.length, 36);
});
// Ensure it generates a nanoID
test('Generate nanoID', () async {
final response = await idGen.generateNanoID();
expect(response.id, isNotNull);
expect(response.id, isNotEmpty);
expect(response.id.length, 10);
});
// Ensure it generates a nanoID with a custom size
test('Generate nanoID with custom size', () async {
final response = await idGen.generateNanoID(size: 20);
print('[IDGen] Generated nanoID with size 20: ${response.id}');
expect(response.id, isNotNull);
expect(response.id, isNotEmpty);
expect(response.id.length, 20);
});
// Ensure it generates a nanoID with a custom alphabet
test('Generate nanoID with custom alphabet', () async {
final response =
await idGen.generateNanoID(alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
print('[IDGen] Generated nanoID with custom alphabet: ${response.id}');
expect(response.id, isNotNull);
expect(response.id, isNotEmpty);
expect(response.id.length, 10);
});
// All together now
test('Generate nanoID with custom size and alphabet', () async {
final response = await idGen.generateNanoID(
size: 20, alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
print(
'[IDGen] Generated nanoID with size 20 and custom alphabet: ${response.id}');
expect(response.id, isNotNull);
expect(response.id, isNotEmpty);
expect(response.id.length, 20);
});
// Ensure it throws an error when generating a nanoID with an empty alphabet
test('Generate nanoID with empty alphabet', () async {
try {
await idGen.generateNanoID(alphabet: '');
fail('Should have thrown an exception');
} on ArgumentError catch (e) {
expect(e.message, 'Cannot generate a nanoID with an empty alphabet');
} catch (e) {
fail('Should have thrown an ArgumentError');
}
});
// Ensure it throws an error when generating a nanoID with an invalid alphabet length
test('Generate nanoID with invalid alphabet length', () async {
try {
await idGen.generateNanoID(alphabet: 'AB');
fail('Should have thrown an exception');
} on ArgumentError catch (e) {
expect(e.message,
'Cannot generate a nanoID with an alphabet of length 2, must be between 3 and 256');
} catch (e) {
fail('Should have thrown an ArgumentError');
}
});
// Ensure it throws an error when generating a nanoID with an invalid size
test('Generate nanoID with invalid size', () async {
try {
await idGen.generateNanoID(size: 0);
fail('Should have thrown an exception');
} on ArgumentError catch (e) {
expect(e.message,
'Cannot generate a nanoID with a length of 0, must be between 1 and 256');
} catch (e) {
fail('Should have thrown an ArgumentError');
}
});
// Ensure it generates a 2FA code pair
test('Generate 2FA code pair', () async {
final response = await idGen.generate2FACode();
print('[IDGen] Generated 2FA code: ${response.id}');
expect(response.id, isNotNull);
expect(response.id, isNotEmpty);
expect(response.id.length, 6);
});
// Ensure it generates a 2FA code pair with a custom length
test('Generate 2FA code pair with custom length', () async {
final response = await idGen.generate2FACode(length: 10);
print('[IDGen] Generated 2FA code with length 10: ${response.id}');
expect(response.id, isNotNull);
expect(response.id, isNotEmpty);
expect(response.id.length, 10);
});
// Ensure it throws an error when generating a 2FA code with an invalid length
test('Generate 2FA code pair with invalid length', () async {
try {
await idGen.generate2FACode(length: 0);
fail('Should have thrown an exception');
} on ArgumentError catch (e) {
expect(e.message,
'Cannot generate a 2FA code with a length of 0, must be between 1 and 256');
} catch (e) {
fail('Should have thrown an ArgumentError');
}
});
// Ensure it generates a license key
test('Generate license key', () async {
final response = await idGen.generateLicenseKey();
print('[IDGen] Generated license key: ${response.id}');
expect(response.id, isNotNull);
expect(response.id, isNotEmpty);
expect(response.id.length, 29);
// Make sure it follows the 5-5-5-5-5 format
final parts = response.id.split('-');
expect(parts.length, 5);
});
// Ensure it generates a word based string
test('Generate word based string', () async {
final response = await idGen.generateWordBasedString();
print('[IDGen] Generated word based string: ${response.id}');
expect(response.id, isNotNull);
expect(response.id, isNotEmpty);
});
// Ensure it generates a word based string with a custom length
test('Generate word based string with custom length', () async {
final response = await idGen.generateWordBasedString(length: 10);
print(
'[IDGen] Generated word based string with length 10: ${response.id}');
expect(response.id, isNotNull);
expect(response.id, isNotEmpty);
// Should contain -'s as the default separator
expect(response.id.contains('-'), isTrue);
});
// Ensure it can generate all separators for word based strings
test('Generate word based string with custom separator', () async {
final responseSlug =
await idGen.generateWordBasedString(separator: 'slug');
print(
'[IDGen] Generated word based string with slug separator: ${responseSlug.id}');
final responseTitle =
await idGen.generateWordBasedString(separator: 'title');
print(
'[IDGen] Generated word based string with title separator: ${responseTitle.id}');
final responseFormal =
await idGen.generateWordBasedString(separator: 'formal');
print(
'[IDGen] Generated word based string with formal separator: ${responseFormal.id}');
expect(responseSlug.id, isNotNull);
expect(responseSlug.id, isNotEmpty);
expect(responseTitle.id, isNotNull);
expect(responseTitle.id, isNotEmpty);
expect(responseFormal.id, isNotNull);
expect(responseFormal.id, isNotEmpty);
});
});
// Ensure it errors when a invalid separator is used for word based strings
test('Generate word based string with invalid separator', () async {
final idGen = IDGen(username: username, token: token);
try {
await idGen.generateWordBasedString(separator: 'invalid');
fail('Should have thrown an exception');
} on ArgumentError catch (e) {
expect(e.message,
'Cannot generate a word based string with an invalid separator');
} catch (e) {
fail('Should have thrown an ArgumentError');
}
});
// Ensure it errors when a invalid length is used for word based strings
test('Generate word based string with invalid length', () async {
final idGen = IDGen(username: username, token: token);
try {
await idGen.generateWordBasedString(length: 0);
fail('Should have thrown an exception');
} on ArgumentError catch (e) {
expect(e.message,
'Cannot generate a word based string with a length of 0, must be between 1 and 16');
} catch (e) {
fail('Should have thrown an ArgumentError');
}
});
// Ensure it generates a snowflake ID
test('Generate snowflake ID', () async {
final idGen = IDGen(username: username, token: token);
final response = await idGen.generateSnowflakeID();
print('[IDGen] Generated snowflake ID: ${response.id}');
expect(response.id, isNotNull);
expect(response.id, isNotEmpty);
expect(response.id.length, 20);
});
// Ensure it generates a keypair
test('Generate keypair', () async {
final idGen = IDGen(username: username, token: token);
final response = await idGen.generateKeypair();
print(
'[IDGen] Generated keypair: ${response.id} and secret: ${response.privateID}');
expect(response.id, isNotNull);
expect(response.id, isNotEmpty);
expect(response.privateID, isNotNull);
expect(response.privateID, isNotEmpty);
});
}