diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml new file mode 100644 index 0000000..180ffd2 --- /dev/null +++ b/.github/workflows/publish-docs.yml @@ -0,0 +1,46 @@ +name: Publish Docs + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +on: + workflow_dispatch: + workflow_run: + workflows: + - Release + types: + - completed + +jobs: + publish-docs: + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0.x' + - name: Install Docfx + run: dotnet tool install docfx -g + - name: Build documentation + run: docfx docs/docfx.json + - name: Setup Pages + uses: actions/configure-pages@v3 + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + with: + path: docs/_site + - name: Deploy to Github Pages + id: deployment + uses: actions/deploy-pages@v1 diff --git a/README.md b/README.md index e239d90..155d1ca 100644 --- a/README.md +++ b/README.md @@ -6,123 +6,193 @@

-Rascal is a simple and lightweight [result type](https://www.youtube.com/watch?v=srQt1NAHYC0&t=1018s) implementation for C#, containing a variety of utilities and standard functions for working with result types and integrating them into the rest of C#. +Rascal is a simple yet powerful [result type](https://www.youtube.com/watch?v=srQt1NAHYC0&t=1018s) implementation for C#, containing a variety of utilities and standard functions for working with result types and integrating them into the rest of C#. Rascal is first and foremost an aggregate of the result types I personally find myself implementing in a majority of my own projects, and a competetor other result libraries second. As such, this library implements some things I think other result implementations are lacking, while omitting some features other libraries do implement. +**TODO**: Link to the Github Pages docfx documentation. +
# Installation + +
+.NET CLI + +Run in a terminal in the root of your project: + ```ps1 -# .NET CLI dotnet add package Rascal --prerelease ``` -# Using the package +
-## The prelude +
+Package manager console -Rascal contains a `Prelude` class (named in reference to most functional languages) which contains a wide variety of utility functions. Since this class contains functions which are used very frequently in code heavily utilizing results, this class is meant to be *imported statically*, i.e. through a `using static` statement. For convenience, this can be included as a global using in a `Usings.cs` file containing other global using statements. +Run from the Visual Studio Package Manager console: -## Samples +```ps1 +NuGet\Install-Package Rascal -IncludePrerelease +``` -### Creating a result -```cs -// Through explicit Ok/Error functions -var explicitOk = Ok("uwu"); -var explicitErr = Err("An error occured."); +
-// Through implicit conversion -Result implicitOk = "owo"; -``` +
+Script environment -### Mapping a result +In environments such as [C# REPL](https://github.com/waf/CSharpRepl) or [RoslynPad](https://roslynpad.net), enter: -"Mapping" refers to taking a result containing some value of type `T` and *mapping* said value to a value of some other type `TNew`. ```cs -// Read input, parse to int, and apply a function to the value -var x = ParseR(Console.ReadLine()!) - .Map(x => Enumerable.Repeat("hewwo", x)); +#r "nuget: Rascal" ``` -
+If you wish to install a specific version of the package, specify the package version: -Another operation, quite similar to mapping, exists, known as a "bind". A bind operation acts like a map, but the function applied to the value of type `T` returns another result, namely a `Result`, which is then returned from the bind. This is the fundamental mechanism which allows chaining result operations together, making for a quite powerful tool. ```cs -// Read input, parse to int, and apply a function to the value, which may fail -var num = ParseR(Console.ReadLine()!); -var den = ParseR(Console.ReadLine()!); +#r "nuget: Rascal, 1.0.1-pre" +``` -var val = num.Then(a => den - .Map(b => DiveSafe(a, b))); +
-static Result DivSafe(int a, int b) => - b != 0 - ? a / b - : "Cannot divide by 0."; -``` +
+PackageReference -The above expression for `val` can alternatively be written using query syntax: -```cs -var val = - from a in num - from b in den - from x in DivSafe(a, b) - select x; +Add under an `ItemGroup` node in your project file: + +```xml + ``` +Obviously, replace `1.0.1-pre` with the actual package version you want. + +
+
-### Various utilities +# Samples + +## Creating results + + + +Results in Rascal can be created in a variety of ways, the two most common of which are through the `Ok` and `Err` methods defined in the prelude, or through implicitly converting ok values or errors into results. -Parse a string or `ReadOnlySpan` to another type, returning a result. `ParseR` (short for `ParseResult`) works for any type implementing `IParsable` or `ISpanParsable`. ```cs -var parsed = ParseR(Console.ReadLine()!); +// You can create a result either through explicit Ok/Error functions... +var explicitOk = Ok(new Person("Melody", 27)); +var explicitError = Err("Could not find person"); + +// ... or through implicit conversions... +Result implicitOk = new Person("Edwin", 32); +Result implicitError = new StringError("Failed to find person"); ``` -
+## Mapping + +"Mapping" refers to taking a result containing some value some type (`T`) and *mapping* said value to a new value of some other type (`TNew`). The principal method of mapping is the aptly named `Map`. -Turn a nullable value into a result. ```cs -var result = F().NotNull(); +var name = "Raymond"; + +// Read console input and try parse it into an int. +// If the input cannot be parsed, the result will be an error. +var age = ParseR(Console.ReadLine()!); -static int? F(); +// Map the age to a new person. +// If the age is an error, the person will also be an error. +var person = age.Map(x => new Person(name, x)); ```
-A function for running another function in a `try` block and returning a result containing either the successful value of the function or the thrown exception. Quite useful for functions which provide no good way of checking whether success is expected before running it, such as IO. `Try` variants are also available for `Map` and `Then`. +Another operation, commonly referred to as "bind" or "chaining", exists, which looks quite similar to mapping, the only difference being that the lambda you supply to the method returns a *new* result rather than a plain value. The principal method of chaining is `Then`, which can be read as "a, then b, then c". + ```cs -var result = Try(() => File.ReadAllText(path)); +// Read console input and assert that it isn't null. +// If the input is null, the value will be an error. +var name = Console.ReadLine().NotNull(); + +// Chain an operation on the name which will only execute if the name is ok. +var person = name.Then(n => +{ + // Read console input, assert that it isn't null, then try parse it into an int. + var age = Console.ReadLine().NotNull() + .Then(str => ParseR(str)); + + // Map the age into a new person. + return age.Map(a => new Person(n, a)); +}); ```
-Validate inputs directly inside a result expression chain, replacing the original value with an error if the predicate fails. +`Map` and `Then` together make up the core of the `Result` type, allowing for chaining multiple operations on a single result. In functional terms, these are what makes `Result` a functor and monad respectively (although not an applicative). + +### Combine + +`Combine` is an addition to `Map` and `Then` which streamlines the specific case where you have two results and want to *combine* them into a single result only if both results are ok. + ```cs -var input = ParseR(Console.ReadLine()!) +// Read console input and assert that it isn't null. +var name = Console.ReadLine().NotNull(); + +// Read console input, assert that it isn't null, then try parse it into an int. +var age = Console.ReadLine().NotNull() + .Then(str => ParseR(str)); + +// Combine the name and age results together, then map them into a person. +var person = name.Combine(age) + .Map(v => new Person(v.first, v.second)); +``` + +## Validation + +Rascal supports a simple way of validating the value of a result, returning an error in case the validation fails. + +```cs +// Read console input, assert that it isn't null, and validate that it matches the regex. +var name = Console.ReadLine().NotNull() .Validate( - x => x >= 0, - x => $"Input {x} is less than 0.") - -// can also be written as -var input = - from x in ParseR(Console.ReadLine()!) - where x >= 0 - select x; + str => Regex.IsMatch(str, "[A-Z][a-z]*"), + _ => "Name can only contain characters a-z and has to start with a capital letter."); + +var person = name.Then(n => +{ + // Read console input, assert that it isn't null, try parse it into an int, then validate that it is greater than 0. + var age = Console.ReadLine().NotNull() + .Then(str => ParseR(str)) + .Validate( + x => x > 0, + _ => "Age has to be greater than 0."); + + return age.Map(a => new Person(n, a)); +}); ``` -
+## Exception handling -### "Unsafe" operations +One of the major kinks of adapting C# into a more functional style (such as using results) is the already existing standard of using exceptions for error-handling. Exceptions have *many* flaws, and result types explicitly exist to provide a better alternative to exceptions, but Rascal nontheless provides a way to interoperate with traditional exception-based error handling. + +The `Try` method in the prelude is the premiere exception-handling method, which runs another function inside a `try`-`catch` block, and returns an `ExceptionError` in case an exception is thrown. -To not be *too* far out-of-line with the rest of C#, there are also functions for accessing the values inside results in an unsafe manner. "Unsafe" in this context is not referring to the `unsafe` from C#, but rather the fact these functions may throw exceptions, as opposed to most other functions which are pure and should not normally throw exceptions. These functions should be treated with care and only be used in situations where the caller knows without a reasonable shadow of a doubt that the operation is safe or an exception is acceptable to be thrown. ```cs -Result result; +// Try read console input and use the input to read the specified file. +// If an exception is thrown, the exception will be returned as an error. +var text = Try(() => +{ + var path = Console.ReadLine()!; + return File.ReadAllText(path); +}); +``` -int x = result.Unwrap(); -// or -int y = (int)result; +`Try` variants also exist for `Map` and `Then`, namely `TryMap` and `ThenTry`. + +```cs +// Read console input and assert that it isn't null. +var path = Console.ReadLine().NotNull(); -int z = result.Expect("Expected result to be successful."); +// Try to map the input by reading a file specified by the input. +// If ReadAllText throws an exception, the exception will be returned as an error. +var text = path.TryMap(p => File.ReadAllText(p)); ``` diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..57510a2 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +_site/ diff --git a/docs/api/.gitignore b/docs/api/.gitignore new file mode 100644 index 0000000..8f5a0e4 --- /dev/null +++ b/docs/api/.gitignore @@ -0,0 +1,2 @@ +.manifest +*.yml diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..6c77e4e --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,9 @@ +See the table of contents for an overview of the library API. + +
+ +## Important types + +- [`Result`](/api/Rascal.Result-1.html) +- [`Prelude`](/api/Rascal.Prelude.html) +- [`Error`](/api/Rascal.Error.html) diff --git a/docs/diagnostics/index.md b/docs/diagnostics/index.md new file mode 100644 index 0000000..e797424 --- /dev/null +++ b/docs/diagnostics/index.md @@ -0,0 +1 @@ +The main Rascal package has a built-in suite of analyzers and code fixes. This section describes the various diagnostics which may be produced by these analyzers. diff --git a/docs/diagnostics/rascal001.md b/docs/diagnostics/rascal001.md new file mode 100644 index 0000000..2f82235 --- /dev/null +++ b/docs/diagnostics/rascal001.md @@ -0,0 +1,31 @@ +# Use 'Map' instead of 'Then(x => Ok(...))' + +
+ +
+Id: RASCAL001 +
+Severity: warning +
+Has code fix: yes +
+
+ +
+ +## Description + +*RASCAL001* is reported when [`Ok`](/api/Rascal.Prelude.html#Rascal_Prelude_Ok__1___0_) or any other form of result construction is used as the immediate return from the lambda inside a [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___) call. Because [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___) chains results, this is equivalent to a much simpler [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) call. The warning can be resolved by replacing the [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___) call with a [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) which maps to the expression inside the [`Ok`](/api/Rascal.Prelude.html#Rascal_Prelude_Ok__1___0_). + +
+ +### Example + +```cs +var a = Ok(2); + +var b = a.Then(x => Ok(F(x))); // RASCAL001 + +// Fix: +var b = a.Map(x => F(x)); +``` diff --git a/docs/diagnostics/rascal002.md b/docs/diagnostics/rascal002.md new file mode 100644 index 0000000..f17366a --- /dev/null +++ b/docs/diagnostics/rascal002.md @@ -0,0 +1,31 @@ +# Use Then instead of 'Map(...).Unnest()' + +
+ +
+Id: RASCAL002 +
+Severity: warning +
+Has code fix: yes +
+
+ +
+ +## Description + +*RASCAL002* is reported when [`Unnest`](/api/Rascal.ResultExtensions.html#Rascal_ResultExtensions_Unnest__1_Rascal_Result_Rascal_Result___0___) is called immediately after a [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) call. This operation is equivalent to calling [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___) with the same mapping function, which improves clarity and performance. The warning can be resolved by removing the [`Unnest`](/api/Rascal.ResultExtensions.html#Rascal_ResultExtensions_Unnest__1_Rascal_Result_Rascal_Result___0___) call and replacing the [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___) call with a [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__). + +
+ +### Example + +```cs +var a = Ok(2); + +var b = a.Map(x => F(x)).Unnest(); // RASCAL002 + +// Fix: +var b = a.Then(x => F(x)); +``` diff --git a/docs/diagnostics/rascal003.md b/docs/diagnostics/rascal003.md new file mode 100644 index 0000000..9f6a9a9 --- /dev/null +++ b/docs/diagnostics/rascal003.md @@ -0,0 +1,31 @@ +# Unnecessary 'Map' call with identity function + +
+ +
+Id: RASCAL003 +
+Severity: warning +
+Has code fix: yes +
+
+ +
+ +## Description + +*RASCAL003* is reported when [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) is used with an *identity function*, i.e. a lambda in the form `x => x`. Because [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) transforms the ok value of a result, applying an identity function onto the value does nothing and returns the same result as the input, and the call is completely useless. The warning can be resolved by removing the call altogether. + +
+ +### Example + +```cs +var a = Ok(2); + +var b = a.Map(x => x); // RASCAL003 + +// Fix: +var b = a; +``` diff --git a/docs/diagnostics/rascal004.md b/docs/diagnostics/rascal004.md new file mode 100644 index 0000000..bda1e15 --- /dev/null +++ b/docs/diagnostics/rascal004.md @@ -0,0 +1,30 @@ +# 'To' called with same type as result + +
+ +
+Id: RASCAL004 +
+Severity: warning +
+Has code fix: no +
+
+ +
+ +## Description + +*RASCAL004* is reported when [`To`](/api/Rascal.Result-1.html#Rascal_Result_1_To__1_Rascal_Error_) is called with the same type as that of the result it is called on. Because `.To()` is equivalent to an `is T` pattern, calling `.To` on a `Result` where both `T`s are the same type will always succeed if the result is ok, and the call is redundant. The warning can be resolved by removing the call altogether. + +
+ +### Example + +```cs +// Types added for clarity + +Result a = Ok(2); + +Result b = a.To(); // RASCAL004 +``` diff --git a/docs/diagnostics/rascal005.md b/docs/diagnostics/rascal005.md new file mode 100644 index 0000000..0a854df --- /dev/null +++ b/docs/diagnostics/rascal005.md @@ -0,0 +1,42 @@ +# 'To' called with impossible type + +
+ +
+Id: RASCAL005 +
+Severity: warning +
+Has code fix: no +
+
+ +
+ +## Description + +*RASCAL005* is reported when [`To`](/api/Rascal.Result-1.html#Rascal_Result_1_To__1_Rascal_Error_) is called with a type which is incompatible with the type of the result. This mainly applies to class types which do not inherit each other, and structs which aren't the same type. Interfaces, type parameters, and `object` may always succeed regardless of which type they are casted to/from. + +
+ +### Example + +```cs +// Types added for clarity + +Result a = Ok(2); + +Result b = a.To(); // RASCAL005 +``` + +```cs +// Types added for clarity + +Result a = Ok(new B()); + +Result b = a.To(); // RASCAL005 + +class A; +class B : A; +class C : A; +``` diff --git a/docs/diagnostics/rascal006.md b/docs/diagnostics/rascal006.md new file mode 100644 index 0000000..f74555f --- /dev/null +++ b/docs/diagnostics/rascal006.md @@ -0,0 +1,40 @@ +# Use 'GetValueOr' instead of 'Match(x => x, ...)' + +
+ +
+Id: RASCAL006 +
+Severity: warning +
+Has code fix: yes +
+
+ +
+ +## Description + +*RASCAL006* is reported when [`Match`](/api/Rascal.Result-1.html#Rascal_Result_1_Match__1_System_Func__0___0__System_Func_Rascal_Error___0__) is called with an *identity function* as its first argument, i.e. a lambda with the form `x => x`. Such a call is equivalent to [`GetValueOr`](/api/Rascal.Result-1.html#Rascal_Result_1_GetValueOr_System_Func_Rascal_Error__0__), with the remaining argument being a function which returns a value based on the result's error. Note that [`GetValueOr`](/api/Rascal.Result-1.html#Rascal_Result_1_GetValueOr_System_Func_Rascal_Error__0__) has three different overloads which are suitable depending on whether the result's error is needed when retrieving the value. The warning can be resolved by removing the first argument and replacing the call to [`Match`](/api/Rascal.Result-1.html#Rascal_Result_1_Match__1_System_Func__0___0__System_Func_Rascal_Error___0__) with [`GetValueOr`](/api/Rascal.Result-1.html#Rascal_Result_1_GetValueOr_System_Func_Rascal_Error__0__). + +
+ +### Example + +```cs +var a = Ok(2); + +var b = a.Match(x => x, e => 0); // RASCAL006 + +// Fix: +var b = a.DefaultOr(0); +``` + +```cs +var a = Ok(2); + +var b = a.Match(x => x, e => F(e)); // RASCAL006 + +// Fix: +var b = a.DefaultOr(e => F(e)); +``` diff --git a/docs/diagnostics/rascal007.md b/docs/diagnostics/rascal007.md new file mode 100644 index 0000000..ea108a9 --- /dev/null +++ b/docs/diagnostics/rascal007.md @@ -0,0 +1,21 @@ +# Missing symbol required for analysis + +
+ +
+Id: RASCAL007 +
+Severity: warning +
+Has code fix: no +
+
+ +
+ +## Description + +*RASCAL007* is reported if any type or member which is required by the analysis suite to perform analysis is found to be missing. The most likely cause of this is referencing the analysis assembly without referencing the core Rascal assembly, or referencing a higher version of the analysis assembly than that of the core assembly. This may also be the result of a bug in the analysis suite. + +> [!IMPORTANT] +> If *RASCAL007* occurs, all analyzers from the analysis suite will completely stop working until the warning is resolved. diff --git a/docs/diagnostics/toc.yml b/docs/diagnostics/toc.yml new file mode 100644 index 0000000..24af9c8 --- /dev/null +++ b/docs/diagnostics/toc.yml @@ -0,0 +1,14 @@ +- name: RASCAL001 + href: rascal001.md +- name: RASCAL002 + href: rascal002.md +- name: RASCAL003 + href: rascal003.md +- name: RASCAL004 + href: rascal004.md +- name: RASCAL005 + href: rascal005.md +- name: RASCAL006 + href: rascal006.md +- name: RASCAL007 + href: rascal007.md diff --git a/docs/docfx.json b/docs/docfx.json new file mode 100644 index 0000000..54cee53 --- /dev/null +++ b/docs/docfx.json @@ -0,0 +1,53 @@ +{ + "metadata": [ + { + "src": [ + { + "src": "../src", + "files": [ + "Rascal/Rascal.csproj" + ] + } + ], + "dest": "api" + } + ], + "build": { + "content": [ + { + "files": [ + "**/*.{md,yml}" + ], + "exclude": [ + "apidocs/**.md", + "_site/**" + ] + } + ], + "resource": [ + { + "files": [ + "images/**" + ] + } + ], + "overwrite": [ + { + "files": [ + "apidocs/**.md" + ] + } + ], + "output": "_site", + "template": [ + "default", + "modern" + ], + "globalMetadata": { + "_appName": "Rascal Docs", + "_appTitle": "Rascal Docs", + "_enableSearch": false, + "pdf": false + } + } +} diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..a61659b --- /dev/null +++ b/docs/index.md @@ -0,0 +1,163 @@ +--- +_layout: landing +--- + +# Rascal + +Rascal is a simple yet powerful [result type](https://www.youtube.com/watch?v=srQt1NAHYC0&t=1018s) implementation for C#, containing a variety of utilities and standard functions for working with result types and integrating them into the rest of C#. + +Rascal is first and foremost an aggregate of the result types I personally find myself implementing in a majority of my own projects, and a competetor other result libraries second. As such, this library implements some things I think other result implementations are lacking, while omitting some features other libraries do implement. + +Additionally, Rascal comes with a suite of analyzers and code fixes to help you write better and more reliable code using the library. The documentation for these analyzers can be found in the [diagnostics documentation](/diagnostics/index.html). + +
+ +## Installation + +# [.NET CLI](#tab/cli) + +Run in a terminal in the root of your project: + +```ps1 +dotnet add package Rascal --prerelease +``` + +# [Package manager console](#tab/pm) + +Run from the Visual Studio Package Manager console: + +```ps1 +NuGet\Install-Package Rascal -IncludePrerelease +``` + +# [Script environment](#tab/repl) + +In environments such as [C# REPL](https://github.com/waf/CSharpRepl) or [RoslynPad](https://roslynpad.net), enter: + +```cs +#r "nuget: Rascal" +``` + +If you wish to install a specific version of the package, specify the package version: + +```cs +#r "nuget: Rascal, 1.0.1-pre" +``` + +# [`PackageReference`](#tab/csproj) + +Add under an `ItemGroup` node in your project file: + +```xml + +``` + +Obviously, replace `1.0.1-pre` with the actual package version you want. + +--- + +
+ +## Quick start + +After installing the package, create a file called `Usings.cs` and add the following: + +```cs +global using static Rascal.Prelude; +``` + +[`Prelude`](/api/Rascal.Prelude.html) includes a variety of utility functions which you can now access from anywhere in your project. + +Now, let's pretend you have an ASP.NET Core app with the following method in a service: + +```cs +public Task GetUser(int userId) +{ + var user = db.Users.FirstOrDefaultAsync(user => user.Id == userId); + + return user; +} +``` + +... you can replace it with: + +```cs +public Task> GetUser(int userId) => TryAsync(async () => +{ + var user = await db.Users.FirstOrDefaultAsync(user => user.Id == userId); + + if (user is null) return new NotFoundError($"User with ID {userId} does not exist."); + + return user; +}); +``` + +This code will handle catching any exceptions thrown by [`FirstOrDefaultAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.data.entity.queryableextensions.firstordefaultasync) and will return a [`NotFoundError`](/api/Rascal.Errors.NotFoundError.html) if the user isn't found. Now you can use this method as such: + +# [Minimal API](#tab/minimal) + +```cs +app.MapGet("/users/{id}", async (int id, IUserService userService) => +{ + var userResult = await userService.GetUser(id); + + return userResult.Match( + user => Results.Ok(user), + error => error switch + { + NotFoundError => Results.NotFound(), + _ => Results.Problem(detail: error.Message, statusCode: 500) + } + ); +}); +``` + +# [Controller](#tab/controller) + +```cs +public sealed class UsersController(IUserService userService) : ControllerBase +{ + [HttpGet("/users/{id}")] + public async Task Get(int id) + { + var user = await userService.GetUser(id); + + return user.Match( + user => this.Ok(user), + IActionResult (error) => error switch + { + NotFoundError => this.NotFound(), + _ => this.Problem(detail: error.Message, statusCode: 500) + } + ); + } +} +``` + +--- + +
+ +### More samples + +A plethora of additional code samples are available in the [samples](/samples/index.html) section of the documentation. + +
+ +### Explore the API + +Once you're ready to dive into the library, feel free to refer to the [API documentation](/api/index.html) for an in-depth look into each of the methods provided by the library. You can of course also explore the API through intellisense in your IDE of choice. + +
+ +## Other great libraries + +Some libraries Rascal takes inspiration from: + +- Rust's [std::result](https://doc.rust-lang.org/std/result) +- Haskell's [Data.Maybe](https://hackage.haskell.org/package/base-4.19.0.0/docs/Data-Maybe.html) and [Control.Monad](https://hackage.haskell.org/package/base-4.19.0.0/docs/Control-Monad.html) +- [Remora.Results](https://github.com/Remora/Remora.Results) +- [Pidgin](https://github.com/benjamin-hodgson/Pidgin) +- [SuperLinq](https://github.com/viceroypenguin/SuperLinq) +- [HonkSharp](https://github.com/asc-community/HonkSharp) +- [error-or](https://github.com/amantinband/error-or) diff --git a/docs/samples/index.md b/docs/samples/index.md new file mode 100644 index 0000000..9c30202 --- /dev/null +++ b/docs/samples/index.md @@ -0,0 +1,64 @@ +This article contains a variety of code samples demonstrating common usages for various parts of the Rascal library. All the samples source code can be found [here](https://github.com/thinker227/Rascal/tree/main/samples). + +
+ +## Creating results + +Results in Rascal can be created in a variety of ways, the two most common of which are through the [`Ok`](/api/Rascal.Prelude.html#Rascal_Prelude_Ok__1___0_) and [`Err`](/api/Rascal.Prelude.html#Rascal_Prelude_Err__1_Rascal_Error_) methods defined in the prelude, or through implicitly converting ok values or errors into results. + +[!code-csharp[](../../samples/Construction.csx#L7-L13)] + +
+ +## Mapping + +"Mapping" refers to taking a result containing some value some type (`T`) and *mapping* said value to a new value of some other type (`TNew`). The principal method of mapping is the aptly named [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__). + +[!code-csharp[](../../samples/Map.csx#L6-L14)] + +
+ +Another operation, commonly referred to as "bind" or "chaining", exists, which looks quite similar to mapping, the only difference being that the lambda you supply to the method returns a *new* result rather than a plain value. The principal method of chaining is [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___), which can be read as "a, then b, then c". + +[!code-csharp[](../../samples/Then.csx#L6-L19)] + +
+ +[`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) and [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___) together make up the core of the [`Result`](/api/Rascal.Result-1.html) type, allowing for chaining multiple operations on a single result. In functional terms, these are what makes [`Result`](/api/Rascal.Result-1.html) a functor and monad respectively (although not an applicative). + +
+ +> [!TIP] +> The aliases [`Select`](/api/Rascal.Result-1.html#Rascal_Result_1_Select__1_System_Func__0___0__) and [`SelectMany`](/api/Rascal.Result-1.html#Rascal_Result_1_SelectMany__1_System_Func__0_Rascal_Result___0___) are available for [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) and [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___) respectively. These exist to supply support for *query expressions* as an alternative to method chaining. Query syntax can in specific situations be more readable than the method chaining alternative, although in *most* scenarios, method chaning is better. [`Select`](/api/Rascal.Result-1.html#Rascal_Result_1_Select__1_System_Func__0___0__) and [`SelectMany`](/api/Rascal.Result-1.html#Rascal_Result_1_SelectMany__1_System_Func__0_Rascal_Result___0___) should ***not*** be used outside query syntax. + +
+ +### Combine + +[`Combine`](/api/Rascal.Result-1.html#Rascal_Result_1_Combine__1_Rascal_Result___0__) is an addition to [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) and [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___) which streamlines the specific case where you have two results and want to *combine* them into a single result only if both results are ok. + +[!code-csharp[](../../samples/Combine.csx#L6-L15)] + +
+ +## Validation + +Rascal supports a simple way of validating the value of a result, returning an error in case the validation fails. + +[!code-csharp[](../../samples/Validation.csx#L8-L24)] + +
+ +## Exception handling + +One of the major kinks of adapting C# into a more functional style (such as using results) is the already existing standard of using exceptions for error-handling. Exceptions have *many* flaws, and result types explicitly exist to provide a better alternative to exceptions, but Rascal nontheless provides a way to interoperate with traditional exception-based error handling. + +The [`Try`](/api/Rascal.Prelude.html#Rascal_Prelude_Try__1_System_Func___0__) method in the prelude is the premiere exception-handling method, which runs another function inside a `try`-`catch` block, and returns an [`ExceptionError`](/api/Rascal.Errors.ExceptionError.html) in case an exception is thrown. + +[!code-csharp[](../../samples/Try.csx#L6-L11)] + +
+ +`Try` variants also exist for [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) and [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___), namely [`TryMap`](/api/Rascal.Result-1.html#Rascal_Result_1_TryMap__1_System_Func__0___0__) and [`ThenTry`](/api/Rascal.Result-1.html#Rascal_Result_1_ThenTry__1_System_Func__0_Rascal_Result___0___). + +[!code-csharp[](../../samples/TryMap.csx#L6-L11)] diff --git a/docs/samples/toc.yml b/docs/samples/toc.yml new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/docs/samples/toc.yml @@ -0,0 +1 @@ +[] diff --git a/docs/toc.yml b/docs/toc.yml new file mode 100644 index 0000000..c378383 --- /dev/null +++ b/docs/toc.yml @@ -0,0 +1,13 @@ +- name: Samples + href: samples/ + homepage: samples/index.md +- name: API + href: api/ + homepage: api/index.md +- name: Diagnostics + href: diagnostics/ + homepage: diagnostics/index.md +- name: Github + href: https://github.com/thinker227/Rascal +- name: Nuget + href: https://www.nuget.org/packages/Rascal diff --git a/samples/.vscode/launch.json b/samples/.vscode/launch.json new file mode 100644 index 0000000..305245b --- /dev/null +++ b/samples/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Script Debug", + "type": "coreclr", + "request": "launch", + "program": "${env:HOME}/.dotnet/tools/dotnet-script", + "args": ["${file}"], + "windows": { + "program": "${env:USERPROFILE}/.dotnet/tools/dotnet-script.exe", + }, + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + } + ] +} \ No newline at end of file diff --git a/samples/Combine.csx b/samples/Combine.csx new file mode 100644 index 0000000..3dbd10e --- /dev/null +++ b/samples/Combine.csx @@ -0,0 +1,19 @@ +#r "../src/Rascal/bin/Release/net8.0/Rascal.dll" + +using Rascal; +using static Rascal.Prelude; + +// Read console input and assert that it isn't null. +var name = Console.ReadLine().NotNull(); + +// Read console input, assert that it isn't null, then try parse it into an int. +var age = Console.ReadLine().NotNull() + .Then(str => ParseR(str)); + +// Combine the name and age results together, then map them into a person. +var person = name.Combine(age) + .Map(v => new Person(v.first, v.second)); + +Console.WriteLine(person); + +record Person(string Name, int Age); diff --git a/samples/Construction.csx b/samples/Construction.csx new file mode 100644 index 0000000..bbad9fa --- /dev/null +++ b/samples/Construction.csx @@ -0,0 +1,15 @@ +#r "../src/Rascal/bin/Release/net8.0/Rascal.dll" + +using Rascal; +using Rascal.Errors; +using static Rascal.Prelude; + +// You can create a result either through explicit Ok/Error functions... +var explicitOk = Ok(new Person("Melody", 27)); +var explicitError = Err("Could not find person"); + +// ... or through implicit conversions... +Result implicitOk = new Person("Edwin", 32); +Result implicitError = new StringError("Failed to find person"); + +record Person(string Name, int Age); diff --git a/samples/Map.csx b/samples/Map.csx new file mode 100644 index 0000000..eaa15e7 --- /dev/null +++ b/samples/Map.csx @@ -0,0 +1,18 @@ +#r "../src/Rascal/bin/Release/net8.0/Rascal.dll" + +using Rascal; +using static Rascal.Prelude; + +var name = "Raymond"; + +// Read console input and try parse it into an int. +// If the input cannot be parsed, the result will be an error. +var age = ParseR(Console.ReadLine()!); + +// Map the age to a new person. +// If the age is an error, the person will also be an error. +var person = age.Map(x => new Person(name, x)); + +Console.WriteLine(person); + +record Person(string Name, int Age); diff --git a/samples/Then.csx b/samples/Then.csx new file mode 100644 index 0000000..e801721 --- /dev/null +++ b/samples/Then.csx @@ -0,0 +1,23 @@ +#r "../src/Rascal/bin/Release/net8.0/Rascal.dll" + +using Rascal; +using static Rascal.Prelude; + +// Read console input and assert that it isn't null. +// If the input is null, the value will be an error. +var name = Console.ReadLine().NotNull(); + +// Chain an operation on the name which will only execute if the name is ok. +var person = name.Then(n => +{ + // Read console input, assert that it isn't null, then try parse it into an int. + var age = Console.ReadLine().NotNull() + .Then(str => ParseR(str)); + + // Map the age into a new person. + return age.Map(a => new Person(n, a)); +}); + +Console.WriteLine(person); + +record Person(string Name, int Age); diff --git a/samples/Try.csx b/samples/Try.csx new file mode 100644 index 0000000..a67db88 --- /dev/null +++ b/samples/Try.csx @@ -0,0 +1,14 @@ +#r "../src/Rascal/bin/Release/net8.0/Rascal.dll" + +using Rascal; +using static Rascal.Prelude; + +// Try read console input and use the input to read the specified file. +// If an exception is thrown, the exception will be returned as an error. +var text = Try(() => +{ + var path = Console.ReadLine()!; + return File.ReadAllText(path); +}); + +Console.WriteLine(text); diff --git a/samples/TryMap.csx b/samples/TryMap.csx new file mode 100644 index 0000000..bcbb803 --- /dev/null +++ b/samples/TryMap.csx @@ -0,0 +1,13 @@ +#r "../src/Rascal/bin/Release/net8.0/Rascal.dll" + +using Rascal; +using static Rascal.Prelude; + +// Read console input and assert that it isn't null. +var path = Console.ReadLine().NotNull(); + +// Try to map the input by reading a file specified by the input. +// If ReadAllText throws an exception, the exception will be returned as an error. +var text = path.TryMap(p => File.ReadAllText(p)); + +Console.WriteLine(text); diff --git a/samples/Validation.csx b/samples/Validation.csx new file mode 100644 index 0000000..a79d587 --- /dev/null +++ b/samples/Validation.csx @@ -0,0 +1,28 @@ +#r "../src/Rascal/bin/Release/net8.0/Rascal.dll" + +using System.Text.RegularExpressions; +using Rascal; +using Rascal.Errors; +using static Rascal.Prelude; + +// Read console input, assert that it isn't null, and validate that it matches the regex. +var name = Console.ReadLine().NotNull() + .Validate( + str => Regex.IsMatch(str, "[A-Z][a-z]*"), + _ => "Name can only contain characters a-z and has to start with a capital letter."); + +var person = name.Then(n => +{ + // Read console input, assert that it isn't null, try parse it into an int, then validate that it is greater than 0. + var age = Console.ReadLine().NotNull() + .Then(str => ParseR(str)) + .Validate( + x => x > 0, + _ => "Age has to be greater than 0."); + + return age.Map(a => new Person(n, a)); +}); + +Console.WriteLine(person); + +record Person(string Name, int Age); diff --git a/samples/omnisharp.json b/samples/omnisharp.json new file mode 100644 index 0000000..3789428 --- /dev/null +++ b/samples/omnisharp.json @@ -0,0 +1,6 @@ +{ + "script": { + "enableScriptNuGetReferences": true, + "defaultTargetFramework": "net8.0" + } +} diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index b01d84c..474b739 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -2,11 +2,14 @@ namespace Rascal.Analysis; public static class Diagnostics { + private const string CorrectnessCategory = "RascalCorrectness"; + private const string AnalysisCategory = "RascalAnalysis"; + public static DiagnosticDescriptor UseMap { get; } = new( "RASCAL001", "Use 'Map' instead of 'Then(x => Ok(...))'", - "Use 'Map' instead of calling 'Ok' directly inside 'Then'", - "Correctness", + "Use 'Map' instead of calling 'Ok' directly inside 'Then'.", + CorrectnessCategory, DiagnosticSeverity.Warning, true, "Calling 'Ok' directly inside a 'Then' call is equivalent to calling 'Map'. " + @@ -14,9 +17,9 @@ public static class Diagnostics public static DiagnosticDescriptor UseThen { get; } = new( "RASCAL002", - "Use Then instead of 'Map(...).Unnest()'", - "Use 'Then' instead of calling 'Unnest' directly after 'Map'", - "Correctness", + "Use 'Then' instead of 'Map(...).Unnest()'", + "Use 'Then' instead of calling 'Unnest' directly after 'Map'.", + CorrectnessCategory, DiagnosticSeverity.Warning, true, "Calling 'Unnest' directly after a 'Map' call is equivalent to calling 'Then'. " + @@ -26,8 +29,8 @@ public static class Diagnostics "RASCAL003", "Unnecessary 'Map' call with identity function", "This call maps {0} to itself. " + - "The call can be safely removed because it doesn't do anything", - "Correctness", + "The call can be safely removed because it doesn't do anything.", + CorrectnessCategory, DiagnosticSeverity.Warning, true, "Calling 'Map' with an identity function returns the same result as the input. " + @@ -38,17 +41,18 @@ public static class Diagnostics "'To' called with same type as result", "This call converts '{0}' to itself and will always succeed. " + "Remove this call to 'To' as it doesn't do anything.", - "Correctness", + CorrectnessCategory, DiagnosticSeverity.Warning, true, - "Calling 'To' with the same type as that of the result will always succeed."); + "Calling 'To' with the same type as that of the result will always succeed. " + + "Remove the call as it doesn't do anything."); public static DiagnosticDescriptor ToImpossibleType { get; } = new( "RASCAL005", "'To' called with impossible type", "This call tries to convert '{0}' to '{1}', but no value of type '{0}' can be of type '{1}'. " + "The conversion will always fail.", - "Correctness", + CorrectnessCategory, DiagnosticSeverity.Warning, true, "Calling 'To' with a type which no value of the type of the result permits will always fail."); @@ -58,18 +62,20 @@ public static class Diagnostics "Use 'GetValueOr' instead of 'Match(x => x, ...)'", "This call matches {0} using an identity function. " + "Use 'GetValueOr' instead to reduce allocations.", - "Correctness", + CorrectnessCategory, DiagnosticSeverity.Warning, true, - "Calling 'Match' with an identity function for the 'ifOk' parameter is equivalent to 'DefaultOr'."); + "Calling 'Match' with an identity function for the 'ifOk' parameter is equivalent to calling 'GetValueOr'. " + + "Replace this call with 'GetValueOr'."); public static DiagnosticDescriptor MissingSymbol { get; } = new( "RASCAL007", "Missing symbol required for analysis", "Cannot find type or member '{0}' which is required for analysis. " + "No analysis will be performed. " + - "Verify that the version of the analyzer package matches that of the library, or report this as a bug.", - "Analysis", + "Verify that the library is referenced and that the version of the analyzer assembly matches that of the library. " + + "Alternatively, this may be a bug and should be reported as such.", + AnalysisCategory, DiagnosticSeverity.Warning, true); } diff --git a/src/Rascal/Rascal.csproj b/src/Rascal/Rascal.csproj index 6293443..c40e86e 100644 --- a/src/Rascal/Rascal.csproj +++ b/src/Rascal/Rascal.csproj @@ -24,7 +24,7 @@ thinker227 thinker227 2023 LICENSE.txt - README.md + package-readme.md true https://github.com/thinker227/Rascal git @@ -32,7 +32,7 @@ - + diff --git a/src/Rascal/ResultExtensions.cs b/src/Rascal/ResultExtensions.cs index d83c3ea..cb23a2e 100644 --- a/src/Rascal/ResultExtensions.cs +++ b/src/Rascal/ResultExtensions.cs @@ -207,4 +207,23 @@ public static async Task> CatchCancellation(this ValueTask task, } } #endif + +#if NET7_0_OR_GREATER // Support for generic math + + // These are just extension variants of their corresponding Prelude methods + // since extension methods in statically imported classes as not accessible. + + /// + [Pure] + public static Result Parse(this string? s, IFormatProvider? provider = null, Error? error = null) + where T : IParsable => + Prelude.ParseR(s, provider, error); + + /// + [Pure] + public static Result Parse(this ReadOnlySpan s, IFormatProvider? provider = null, Error? error = null) + where T : ISpanParsable => + Prelude.ParseR(s, provider, error); + +#endif } diff --git a/src/Rascal/package-readme.md b/src/Rascal/package-readme.md new file mode 100644 index 0000000..7badc63 --- /dev/null +++ b/src/Rascal/package-readme.md @@ -0,0 +1,5 @@ +Rascal is a simple yet powerful [result type](https://www.youtube.com/watch?v=srQt1NAHYC0&t=1018s) implementation for C#, containing a variety of utilities and standard functions for working with result types and integrating them into the rest of C#. + +Rascal is first and foremost an aggregate of the result types I personally find myself implementing in a majority of my own projects, and a competetor other result libraries second. As such, this library implements some things I think other result implementations are lacking, while omitting some features other libraries do implement. + +**TODO**: Link to the Github Pages docfx documentation.