added fpdart demo

This commit is contained in:
2024-11-27 15:59:08 +01:00
parent 89e12bdf9f
commit caf6007c26
10 changed files with 755 additions and 254 deletions
@@ -0,0 +1,3 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
@@ -0,0 +1 @@
none
+44
View File
@@ -0,0 +1,44 @@
# **🚀 Demo-Projekt: AppError und Funktionale Programmierung mit fpdart**
Dieses Demo-Projekt begleitet die Zwischenpräsentation und verdeutlicht, wie funktionale Programmierung mit Dart und der Bibliothek `fpdart` umgesetzt werden kann. Es greift die zentralen Konzepte der Präsentation auf.
Das Projekt zeigt, wie durch funktionale Ansätze mit `fpdart`:
- ✅ Logik deklarativ ausgedrückt wird.
- 🛠️ Fehler elegant und strukturiert behandelt werden.
- 🔄 Erweiterbare, wartbare und typsichere Programme entstehen.
## **🔑 Zentrale Aspekte**
### **1. Monaden als Datenkonstrukte**
- 📦 Anwendung von **`Either`** für synchrone Fehler- und Erfolgsverarbeitung.
- ⏳ Verwendung von **`TaskEither`** zur Kombination von Lazy Evaluation mit asynchroner Verarbeitung und Fehlerbehandlung.
### **2. 🛡️ `AppError`: Einheitliche Fehlerstruktur**
- Einführung eines stark typisierten `AppError`, um Fehler systematisch zu kategorisieren (z. B. `ValidationError`, `DatabaseError`, `NetworkError`).
- Klare Trennung von:
- 📄 **Nachricht** (für Benutzer)
- ⚙️ **Typ** (zur Kategorisierung)
- 🔍 **Details** (für Debugging)
### **3. Praxisnahe Beispiele**
- 🔍 **Validierung**: Prüfung von Benutzereingaben mit `Either`.
- 🛠️ **Fehlerbehandlung**: Nutzung von `TaskEither` für Datenbank- und Netzwerkfehler.
- 🔗 **Verkettung von Funktionen**:
- Nahtlose Kombination von Validierung, Speicherung und weiteren Prozessen.
- Kein Abbruch bei Zwischenfehlern.
### **4. Verbindung zu funktionalen Konzepten**
- 🏛️ **Ursprung**: Konzepte aus Haskell und FP-TS.
- 🌀 **Monaden**: Praktische Anwendung zur sicheren Datenverarbeitung ohne Seiteneffekte.
- 🌐 **Anwendungsbezug**: Funktionale Programmierung als Werkzeug für robuste und skalierbare Software.
Das Projekt dient als praktische Grundlage, um die in der Präsentation behandelten Konzepte von funktionaler Programmierung und Fehlerbehandlung greifbar zu machen. Es zeigt, wie mit `fpdart`:
- Der Code sauber und wartbar bleibt.
- Funktionale Programmierung in Dart effektiv umgesetzt wird.
- Fehlermanagement nicht nur robust, sondern auch elegant sein kann.
@@ -0,0 +1,30 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.
include: package:lints/recommended.yaml
# Uncomment the following section to specify additional rules.
# linter:
# rules:
# - camel_case_types
# analyzer:
# exclude:
# - path/to/excluded/files/**
# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options
@@ -0,0 +1,46 @@
import 'package:fpdart/fpdart.dart';
// Fehlerkategorien
enum AppErrorType {
ValidationError,
DatabaseError,
NetworkError,
UnknownError,
}
class AppError {
final String message; // Benutzerfreundliche Fehlermeldung
final AppErrorType type; // Typisierte Fehlerkategorie
final Exception? exception; // Originale Exception (falls vorhanden)
AppError({
required this.message,
this.type = AppErrorType.UnknownError,
this.exception,
});
@override
String toString() {
return 'AppError(message: $message, type: $type, exception: $exception)';
}
/// Hilfsmethode zur einfachen Erstellung eines `Left`-Werts für `Either`
static Either<AppError, T> left<T>(
String message, {
AppErrorType? type,
Exception? exception,
}) {
return Either.left(AppError(
message: message,
type: type ?? AppErrorType.UnknownError,
exception: exception,
));
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is AppError && other.message == message && other.type == type;
}
}
@@ -0,0 +1,68 @@
import 'package:demo/app_error.dart';
import 'package:fpdart/fpdart.dart';
// Mock-Datenbank
final existingEmails = ["existing@example.com"];
// Validierungsfunktionen
Either<AppError, String> validateName(String name) {
return name.isNotEmpty
? Either.right(name)
: AppError.left(
"Name cannot be empty",
type: AppErrorType.ValidationError,
);
}
Either<AppError, String> validateEmail(String email) {
final emailRegex = RegExp(r"^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+");
return emailRegex.hasMatch(email)
? Either.right(email)
: AppError.left(
"Invalid email format",
type: AppErrorType.ValidationError,
);
}
// Asynchrone Überprüfung, ob die E-Mail bereits existiert
TaskEither<AppError, bool> checkEmailExists(String email) {
return TaskEither.tryCatch(
() async => existingEmails.contains(email),
(error, _) => AppError(
message: "Error checking email existence",
type: AppErrorType.DatabaseError,
exception: error as Exception?,
),
);
}
// Benutzer in der Datenbank speichern (Dummy-Implementierung)
TaskEither<AppError, bool> saveUser(String name, String email) {
return TaskEither.tryCatch(
() async {
await Future.delayed(Duration(seconds: 1)); // Simulierte Latenz
print("User saved: $name, $email");
return true;
},
(error, _) => AppError(
message: "Error saving user",
type: AppErrorType.DatabaseError,
exception: error as Exception?,
),
);
}
// Optional: Begrüßungsnachricht senden
TaskEither<AppError, bool> sendWelcomeEmail(String email) {
return TaskEither.tryCatch(
() async {
print("Welcome email sent to $email");
return true;
},
(error, _) => AppError(
message: "Error sending welcome email",
type: AppErrorType.NetworkError,
exception: error as Exception?,
),
);
}
+410
View File
@@ -0,0 +1,410 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
url: "https://pub.dev"
source: hosted
version: "73.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
url: "https://pub.dev"
source: hosted
version: "6.8.0"
args:
dependency: transitive
description:
name: args
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev"
source: hosted
version: "2.6.0"
async:
dependency: transitive
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.12.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: "4b03e11f6d5b8f6e5bb5e9f7889a56fe6c5cbe942da5378ea4d4d7f73ef9dfe5"
url: "https://pub.dev"
source: hosted
version: "1.11.0"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fpdart:
dependency: "direct main"
description:
name: fpdart
sha256: "1b84ce64453974159f08046f5d05592020d1fcb2099d7fe6ec58da0e7337af77"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
io:
dependency: transitive
description:
name: io
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
js:
dependency: transitive
description:
name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
source: hosted
version: "0.7.1"
lints:
dependency: "direct dev"
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
macros:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
url: "https://pub.dev"
source: hosted
version: "2.0.1"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
url: "https://pub.dev"
source: hosted
version: "0.10.12"
source_span:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.2"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
test:
dependency: "direct dev"
description:
name: test
sha256: f2a018e2baa6fce7c8daa55b8bdf4b3d7d165f82caac269e4cbe5edd666c0e4c
url: "https://pub.dev"
source: hosted
version: "1.25.9"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.4"
test_core:
dependency: transitive
description:
name: test_core
sha256: "60ff490bb383858015df7b7a0d883301a426edf9033989f55f091d91efb9dfaf"
url: "https://pub.dev"
source: hosted
version: "0.6.6"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
url: "https://pub.dev"
source: hosted
version: "14.3.1"
watcher:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.5.4 <4.0.0"
@@ -0,0 +1,16 @@
name: demo
description: A sample command-line application.
version: 1.0.0
# repository: https://github.com/my_org/my_repo
environment:
sdk: ^3.5.4
# Add regular dependencies here.
dependencies:
fpdart: ^1.1.1
# path: ^1.8.0
dev_dependencies:
lints: ^4.0.0
test: ^1.24.0
@@ -0,0 +1,137 @@
import 'package:demo/app_error.dart';
import 'package:demo/demo.dart';
import 'package:fpdart/fpdart.dart';
import 'package:test/test.dart';
void main() {
group("User Registration Tests", () {
test("Valid name and email results in successful registration", () async {
final name = "John Doe";
final email = "newuser@example.com";
final result =
await Either<AppError, (String, String)>.right((name, email))
.flatMap((userData) => validateName(userData.$1).flatMap(
(validName) => validateEmail(userData.$2)
.map((validEmail) => (validName, validEmail))))
.toTaskEither()
.flatMap((validUser) => checkEmailExists(validUser.$2).flatMap(
(exists) => exists
? AppError.left("Email already exists",
type: AppErrorType.ValidationError)
.toTaskEither()
: saveUser(validUser.$1, validUser.$2)))
.flatMap((_) => sendWelcomeEmail(email))
.run();
result.match(
(error) => fail("Test failed with error: ${error.message}"),
(success) => expect(success, equals(true)),
);
});
test("Invalid email format returns validation error", () async {
final name = "John Doe";
final email = "invalid-email";
final result =
await Either<AppError, (String, String)>.right((name, email))
.flatMap((userData) => validateName(userData.$1).flatMap(
(validName) => validateEmail(userData.$2)
.map((validEmail) => (validName, validEmail))))
.toTaskEither()
.run();
result.match(
(error) {
expect(error.message, equals("Invalid email format"));
expect(error.type, equals(AppErrorType.ValidationError));
},
(success) => fail("Test succeeded unexpectedly"),
);
});
test("Existing email results in error", () async {
final name = "John Doe";
final email = "existing@example.com";
final result =
await Either<AppError, (String, String)>.right((name, email))
.flatMap((userData) => validateName(userData.$1).flatMap(
(validName) => validateEmail(userData.$2)
.map((validEmail) => (validName, validEmail))))
.toTaskEither()
.flatMap((validUser) => checkEmailExists(validUser.$2).flatMap(
(exists) => exists
? AppError.left("Email already exists",
type: AppErrorType.ValidationError)
.toTaskEither()
: saveUser(validUser.$1, validUser.$2)))
.run();
result.match(
(error) => expect(error.message, equals("Email already exists")),
(success) => fail("Test succeeded unexpectedly"),
);
});
test("Empty name returns validation error", () async {
final name = "";
final email = "newuser@example.com";
final result =
await Either<AppError, (String, String)>.right((name, email))
.flatMap((userData) => validateName(userData.$1).flatMap(
(validName) => validateEmail(userData.$2)
.map((validEmail) => (validName, validEmail))))
.toTaskEither()
.run();
result.match(
(error) => expect(
error,
equals(AppError(
message: "Name cannot be empty",
type: AppErrorType.ValidationError))),
(success) => fail("Test succeeded unexpectedly"),
);
});
test("Error while sending welcome email is handled gracefully", () async {
final name = "John Doe";
final email = "newuser@example.com";
// Simuliere einen Fehler beim Senden der Begrüßungs-E-Mail
TaskEither<AppError, bool> sendWelcomeEmail(String email) {
return TaskEither.left(AppError(
message: "Error sending welcome email",
type: AppErrorType.NetworkError,
));
}
final result =
await Either<AppError, (String, String)>.right((name, email))
.flatMap((userData) => validateName(userData.$1).flatMap(
(validName) => validateEmail(userData.$2)
.map((validEmail) => (validName, validEmail))))
.toTaskEither()
.flatMap((validUser) =>
checkEmailExists(validUser.$2).flatMap((exists) => exists
? AppError.left(
"Email already exists",
type: AppErrorType.ValidationError,
).toTaskEither()
: saveUser(validUser.$1, validUser.$2)))
.flatMap((_) => sendWelcomeEmail(email))
.run();
result.match(
(error) {
expect(error.message, equals("Error sending welcome email"));
expect(error.type, equals(AppErrorType.NetworkError));
},
(success) => fail("Test succeeded unexpectedly"),
);
});
});
}