From 8389ec0f09d1e96a507b382c320b852214962ddd Mon Sep 17 00:00:00 2001 From: Yuriy Durov Date: Wed, 10 Jan 2024 19:52:07 +0400 Subject: [PATCH] QoL improvements commit 0de3abc4a7026f68cbaf0cb57ea97d2fc6210399 Author: Yuriy Durov Date: Wed Jan 10 19:49:17 2024 +0400 Styling commit 2145e484fd21dcf54da86cc8d02c6619435cf631 Author: Yuriy Durov Date: Wed Jan 10 19:47:21 2024 +0400 Update documentation website commit 8ffbe6ed26d6a8b5257b17cc57a7441468e38c7c Author: Yuriy Durov Date: Wed Jan 10 19:32:44 2024 +0400 Improvements to OCPI endpoint version parsing --- .github/workflows/docs.yml | 74 ++++++++------- README.md | 9 +- docs/_config.yml | 6 -- docs/book.toml | 2 + docs/{ => src}/1.introduction.md | 10 --- docs/{ => src}/2.architecture.md | 14 +-- docs/{ => src}/3.implementation.md | 14 +-- docs/{ => src}/4.contracts.md | 14 +-- docs/{ => src}/5.error-handling.md | 14 +-- docs/{ => src}/6.pagination.md | 12 --- docs/{ => src}/7.versioning.md | 7 -- docs/{ => src}/README.md | 0 docs/src/SUMMARY.md | 9 ++ sample/OCPI.Net.Sample/appsettings.json | 3 +- .../Extensions/AddOcpiVersioningExtension.cs | 1 + .../Services/OcpiVersionService.cs | 89 +++++++++++++++++-- .../Enums/Versioning/OcpiVersion.cs | 4 +- 17 files changed, 138 insertions(+), 144 deletions(-) delete mode 100644 docs/_config.yml create mode 100644 docs/book.toml rename docs/{ => src}/1.introduction.md (96%) rename docs/{ => src}/2.architecture.md (91%) rename docs/{ => src}/3.implementation.md (91%) rename docs/{ => src}/4.contracts.md (89%) rename docs/{ => src}/5.error-handling.md (95%) rename docs/{ => src}/6.pagination.md (94%) rename docs/{ => src}/7.versioning.md (97%) rename docs/{ => src}/README.md (100%) create mode 100644 docs/src/SUMMARY.md diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a142169..de4b153 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,50 +1,46 @@ -# Sample workflow for building and deploying a Jekyll site to GitHub Pages -name: Deploy Jekyll with GitHub Pages dependencies preinstalled +name: Publish Documentation on: + # Runs on pushes targeting the default branch push: - branches: ["main"] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false + branches: + - main + - updates + paths: + - "docs/**" + - ".github/workflows/docs.yml" jobs: - # Build job - build: + deploy: runs-on: ubuntu-latest + permissions: + contents: write # To push a branch + pages: write # To push to a GitHub Pages site + id-token: write # To update the deployment status steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Pages - uses: actions/configure-pages@v3 - - name: Build with Jekyll - uses: actions/jekyll-build-pages@v1 + - uses: actions/checkout@v4 with: - source: ./docs/ - destination: ./_site + fetch-depth: 0 + - name: Install latest mdbook + run: | + tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name') + url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz" + mkdir mdbook + curl -sSL $url | tar -xz --directory=./mdbook + echo `pwd`/mdbook >> $GITHUB_PATH + - name: Build Book + run: | + # This assumes your book is in the root of your repository. + # Just add a `cd` here if you need to change to another directory. + cd docs + mdbook build + - name: Setup Pages + uses: actions/configure-pages@v2 - name: Upload artifact - uses: actions/upload-pages-artifact@v2 - - # Deployment job - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: + uses: actions/upload-pages-artifact@v1 + with: + # Upload entire repository + path: 'docs/book' - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v2 \ No newline at end of file diff --git a/README.md b/README.md index 00bfed1..c556eb1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ -# OCPI.Net - ![Tests](https://github.com/BitzArt/OCPI.Net/actions/workflows/Tests.yml/badge.svg) +[![NuGet version](https://img.shields.io/nuget/v/Ocpi.Net.svg)](https://www.nuget.org/packages/Ocpi.Net/) +[![NuGet downloads](https://img.shields.io/nuget/dt/Ocpi.Net.svg)](https://www.nuget.org/packages/Ocpi.Net/) + [OCPI (Open Charge Point Interface)](https://github.com/ocpi/ocpi) implementation for .Net, built with C# | OCPI Version | Support | Progress | @@ -11,9 +12,7 @@ | 2.1.1 | Planned | (https://github.com/BitzArt/OCPI.Net/issues/16) | | 2.1 | Planned | (https://github.com/BitzArt/OCPI.Net/issues/17) | -## Resources - -[![documentation](https://img.shields.io/badge/OCPI.Net_documentation-%230072C6?style=for-the-badge)](https://bitzart.github.io/OCPI.Net/1.introduction.html) +[![documentation](https://img.shields.io/badge/documentation-%230072C6?style=for-the-badge)](https://bitzart.github.io/OCPI.Net/1.introduction.html) ## License diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 1db7a40..0000000 --- a/docs/_config.yml +++ /dev/null @@ -1,6 +0,0 @@ -remote_theme: pages-themes/midnight@v0.2.0 -plugins: -- jekyll-remote-theme # add this line to the plugins list if you already have one - -title: OCPI.Net -description: documentation \ No newline at end of file diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 0000000..4cbf09b --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,2 @@ +[book] +title = "OCPI.Net documentation" \ No newline at end of file diff --git a/docs/1.introduction.md b/docs/src/1.introduction.md similarity index 96% rename from docs/1.introduction.md rename to docs/src/1.introduction.md index d2fc275..c7b7f78 100644 --- a/docs/1.introduction.md +++ b/docs/src/1.introduction.md @@ -1,7 +1,3 @@ -Back to the [Table of Contents](README.md) - ---- - ## Introduction Welcome to the documentation for the OCPI.Net package! This documentation is organized into several [sections](README.md), each covering a different aspect of the package. In this section, we'll provide an overview of the package and its benefits, as well as a guide to getting started with OCPI.Net. @@ -47,9 +43,3 @@ Make sure to check out this [Sample application](https://github.com/BitzArt/OCPI 3. Enjoy the power of OCPI.Net! You will find more info on how to use the package's functionality in the later sections of this documentation. See [Implementation](3.implementation.md) for more information on how to implement OCPI modules using OCPI.Net. - - ---- - -Next topic: -[Architecture](2.architecture.md) diff --git a/docs/2.architecture.md b/docs/src/2.architecture.md similarity index 91% rename from docs/2.architecture.md rename to docs/src/2.architecture.md index 236eabc..6ce1262 100644 --- a/docs/2.architecture.md +++ b/docs/src/2.architecture.md @@ -1,10 +1,3 @@ -Back to the [Table of Contents](README.md) - -Previous topic: -[Introduction](1.introduction.md) - ---- - ## Architecture The OCPI.Net package is designed to be as flexible as possible, allowing you to implement OCPI modules in any way you see fit. The package uses Asp.Net Core Controllers for handling incoming OCPI requests and returning OCPI responses. The package also contains a number of services which are used to perform various tasks such as validating incoming requests, generating responses, and handling errors. @@ -21,9 +14,4 @@ The package uses Exceptions as a way to handle errors. The exceptions can be thr ### Integration with Asp.Net Core -The OCPI.Net package is designed to be used with Asp.Net Core. The package contains a `OcpiController` base class which you can use to implement your own OCPI controllers. This class contains a number of methods which you can use to handle incoming OCPI requests and generate OCPI responses. - ---- - -Next topic: -[Implementation](3.implementation.md) \ No newline at end of file +The OCPI.Net package is designed to be used with Asp.Net Core. The package contains a `OcpiController` base class which you can use to implement your own OCPI controllers. This class contains a number of methods which you can use to handle incoming OCPI requests and generate OCPI responses. \ No newline at end of file diff --git a/docs/3.implementation.md b/docs/src/3.implementation.md similarity index 91% rename from docs/3.implementation.md rename to docs/src/3.implementation.md index 21c2338..d4f8209 100644 --- a/docs/3.implementation.md +++ b/docs/src/3.implementation.md @@ -1,10 +1,3 @@ -Back to the [Table of Contents](README.md) - -Previous topic: -[Architecture](2.architecture.md) - ---- - ## Implementation You can always refer to the [Sample application](https://github.com/BitzArt/OCPI.Net/tree/main/sample/OCPI.Net.Sample) for a working example of how to use the package. @@ -50,9 +43,4 @@ The package is designed to be as flexible as possible, allowing you to implement You are not forced to use any of these services, and you can implement your own services if you wish. The package also allows you to override some of the default services with your own custom implementations. -Refer to the [Architecture](2.architecture.md) section for more information on the package's internal architecture. - ---- - -Next topic: -[OCPI Contracts](4.contracts.md) +Refer to the [Architecture](2.architecture.md) section for more information on the package's internal architecture. \ No newline at end of file diff --git a/docs/4.contracts.md b/docs/src/4.contracts.md similarity index 89% rename from docs/4.contracts.md rename to docs/src/4.contracts.md index 01587c5..a41178f 100644 --- a/docs/4.contracts.md +++ b/docs/src/4.contracts.md @@ -1,10 +1,3 @@ -Back to the [Table of Contents](README.md) - -Previous topic: -[Implementation](3.implementation.md) - ---- - ## OCPI Contracts The package contains a number of classes which are used to represent OCPI objects. You can see the full list of contract models [here](https://github.com/BitzArt/OCPI.Net/tree/main/src/OCPI.Net.Contracts). @@ -39,9 +32,4 @@ public class OcpiLocationsReceiverController : OcpiController } ``` -Validation functionality is built on top of [FluentValidation](https://github.com/FluentValidation/FluentValidation) nuget package. - ---- - -Next topic: -[Error handling](5.error-handling.md) \ No newline at end of file +Validation functionality is built on top of [FluentValidation](https://github.com/FluentValidation/FluentValidation) nuget package. \ No newline at end of file diff --git a/docs/5.error-handling.md b/docs/src/5.error-handling.md similarity index 95% rename from docs/5.error-handling.md rename to docs/src/5.error-handling.md index a16b184..19236da 100644 --- a/docs/5.error-handling.md +++ b/docs/src/5.error-handling.md @@ -1,10 +1,3 @@ -Back to the [Table of Contents](README.md) - -Previous topic: -[OCPI Contracts](4.contracts.md) - ---- - ## Error Handling The package uses Exceptions as a way to handle errors. The exceptions can be thrown from anywhere in your code, and the package will automatically handle them and generate the appropriate OCPI response. Exception handling is done by the `OcpiExceptionFilter` attribute, which is automatically applied to all controllers that inherit from the `OcpiController` base class. Exceptions can be populated with a custom data payload, which will be included in the OCPI response. @@ -54,9 +47,4 @@ public class MyCustomOcpiException : OcpiExceptionBase { } } -``` - ---- - -Next topic: -[Pagination](6.pagination.md) \ No newline at end of file +``` \ No newline at end of file diff --git a/docs/6.pagination.md b/docs/src/6.pagination.md similarity index 94% rename from docs/6.pagination.md rename to docs/src/6.pagination.md index 21ca172..31ad658 100644 --- a/docs/6.pagination.md +++ b/docs/src/6.pagination.md @@ -1,10 +1,3 @@ -Back to the [Table of Contents](README.md) - -Previous topic: -[Error Handling](5.error-handling.md) - ---- - ## Pagination The OCPI.Net package uses some classes from the [BitzArt.Pagination](https://github.com/BitzArt/Pagination) nuget package to handle paginated endpoints. Refer to it for guidance on how to use the pagination functionality. @@ -72,8 +65,3 @@ public class OcpiLocationsSenderController : OcpiController }; } ``` - ---- - -Next topic: -[Versioning](7.versioning.md) \ No newline at end of file diff --git a/docs/7.versioning.md b/docs/src/7.versioning.md similarity index 97% rename from docs/7.versioning.md rename to docs/src/7.versioning.md index 03900b5..bbb8020 100644 --- a/docs/7.versioning.md +++ b/docs/src/7.versioning.md @@ -1,10 +1,3 @@ -Back to the [Table of Contents](README.md) - -Previous topic: -[Pagination](6.pagination.md) - ---- - ## Versioning The OCPI.Net package supports multiple OCPI versions at the same time. The package uses the `OcpiEndpoint` attribute to determine which OCPI versions are supported by a given controller. diff --git a/docs/README.md b/docs/src/README.md similarity index 100% rename from docs/README.md rename to docs/src/README.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md new file mode 100644 index 0000000..424d2c3 --- /dev/null +++ b/docs/src/SUMMARY.md @@ -0,0 +1,9 @@ +# Summary + +- [Introduction](1.introduction.md) +- [Architecture](2.architecture.md) +- [Implementation](3.implementation.md) +- [OCPI Contracts](4.contracts.md) +- [Error Handling](5.error-handling.md) +- [Pagination](6.pagination.md) +- [Versioning](7.versioning.md) \ No newline at end of file diff --git a/sample/OCPI.Net.Sample/appsettings.json b/sample/OCPI.Net.Sample/appsettings.json index 56c7152..e18ebe5 100644 --- a/sample/OCPI.Net.Sample/appsettings.json +++ b/sample/OCPI.Net.Sample/appsettings.json @@ -2,7 +2,8 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Warning", + "OCPI.Versioning.Services.OcpiVersionService": "Debug" } }, "OCPI": { diff --git a/src/OCPI.Net.Controllers/Extensions/AddOcpiVersioningExtension.cs b/src/OCPI.Net.Controllers/Extensions/AddOcpiVersioningExtension.cs index f8f39ec..0a95bd9 100644 --- a/src/OCPI.Net.Controllers/Extensions/AddOcpiVersioningExtension.cs +++ b/src/OCPI.Net.Controllers/Extensions/AddOcpiVersioningExtension.cs @@ -10,6 +10,7 @@ public static WebApplicationBuilder AddOcpiVersioning(this WebApplicationBuilder { builder.AddOcpiOptions(); + builder.Services.AddLogging(); builder.Services.AddSingleton(); return builder; diff --git a/src/OCPI.Net.Controllers/Services/OcpiVersionService.cs b/src/OCPI.Net.Controllers/Services/OcpiVersionService.cs index 61113af..64aba2c 100644 --- a/src/OCPI.Net.Controllers/Services/OcpiVersionService.cs +++ b/src/OCPI.Net.Controllers/Services/OcpiVersionService.cs @@ -1,5 +1,6 @@ using BitzArt.EnumToMemberValue; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using OCPI.Contracts; namespace OCPI.Versioning.Services; @@ -7,14 +8,18 @@ namespace OCPI.Versioning.Services; internal class OcpiVersionService : IOcpiVersionService { private readonly OcpiOptions _options; + private readonly ILogger _logger; private readonly ICollection _routeMaps; private readonly IDictionary> _versionRouteMaps; - public OcpiVersionService(OcpiOptions options) + public OcpiVersionService(OcpiOptions options, ILogger logger) { _options = options; + _logger = logger; + + _logger.LogInformation("Initializing OcpiVersionService"); _routeMaps = new HashSet(); _versionRouteMaps = new Dictionary>(); @@ -24,9 +29,14 @@ public OcpiVersionService(OcpiOptions options) .GetAssemblies() .SelectMany(x => x.DefinedTypes) .Where(x => !x.IsAbstract) - .Where(x => typeof(OcpiController).IsAssignableFrom(x)); + .Where(x => typeof(OcpiController).IsAssignableFrom(x)) + .ToList(); + + _logger.LogInformation("{count} OCPI Controllers found", controllerTypes.Count); foreach (var type in controllerTypes) ParseEndpoint(type); + + _logger.LogInformation("OCPI Controllers parsed successfully"); } public IEnumerable GetRoutes() => _routeMaps; @@ -60,10 +70,15 @@ private OcpiVersionInfo GetVersionInfo(OcpiVersion version, string template) public OcpiVersionDetails GetVersionDetails(string request) { - var version = request.ToEnum(OcpiVersion.Invalid); - if (version == OcpiVersion.Invalid) throw OcpiException.ClientError($"Invalid OCPI Version: '{request}'."); - - return GetVersionDetails(version); + try + { + var version = request.ToEnum(); + return GetVersionDetails(version); + } + catch (Exception ex) + { + throw OcpiException.ClientError($"Invalid OCPI Version: '{request}'.", ex); + } } public OcpiVersionDetails GetVersionDetails(OcpiVersion request) @@ -118,12 +133,13 @@ private void ParseEndpoint(Type type) var endpoint = GetAttribute(type); if (endpoint is null) return; - var routes = GetAttributes(type).Select(x => x.Template); + _logger.LogDebug("Parsing OCPI endpoints for Controller {ControllerName}...", type.Name); + + var routes = GetAttributes(type).Select(x => x.Template).ToList(); foreach (var version in endpoint.Versions) { - var route = routes.FirstOrDefault(x => x.StartsWith(version.ToMemberValue())); - if (route is null) throw new Exception($"Appropriate route not found for Controller {type.Name} and OCPI Version {version.ToMemberValue()}"); + var route = FindRouteForVersion(version, endpoint.Versions, routes, type.Name); foreach (var role in endpoint.Roles) { @@ -141,6 +157,61 @@ private void ParseEndpoint(Type type) } } + private string FindRouteForVersion(OcpiVersion version, IEnumerable allVersions, IEnumerable routes, string controllerName) + { + var target = version.ToMemberValue(); + + _logger.LogDebug("Attempting to find a matching route for OCPI Version {version}", target); + + var otherVersions = allVersions + .Except(new[] { version }) + .Select(x => x.ToMemberValue()) + .ToList(); + + var badMatches = new List(); + + foreach (var route in routes) + { + if (!route.Contains(target)) continue; + + _logger.LogDebug("Route '{route}' matched with OCPI Version {target}", route, target); + + var otherMatches = otherVersions.Where(route.Contains).ToList(); + if (otherMatches.Any()) + { + _logger.LogDebug("Route '{route}' also matched with other OCPI Versions this endpoint implements.", route); + + if (otherMatches.All(target.Contains)) + { + _logger.LogDebug("Version conflict successfully resolved, using route '{route}'", route); + return route; + } + + _logger.LogDebug("Version conflict could not be resolved, keeping '{route}' as bad match for version '{version}'", route, target); + badMatches.Add(route); + continue; + } + + _logger.LogDebug("A good match was found!"); + + return route; + } + + if (!badMatches.Any()) throw new RouteNotFoundForOcpiVersionException($"No matching route was found for Controller {controllerName} and OCPI Version {version.ToMemberValue()}"); + + _logger.LogWarning("No good matches were found for Endpoint {endpoint} and OcpiVersion {target}, using a bad match.", controllerName, target); + return badMatches.First(); + } + + internal class RouteNotFoundForOcpiVersionException : Exception + { + public RouteNotFoundForOcpiVersionException(string controllerName, string version) : base($"Unable to find a matching route for Controller {controllerName} and OCPI Version {version}") { } + + public RouteNotFoundForOcpiVersionException(string message) : base(message) + { + } + } + private void AddToVersionMap(OcpiVersion version, OcpiEndpointRouteMap route) { var versionPresent = _versionRouteMaps.TryGetValue(version, out var map); diff --git a/src/OCPI.Net.Core/Enums/Versioning/OcpiVersion.cs b/src/OCPI.Net.Core/Enums/Versioning/OcpiVersion.cs index 6f0b130..8f51713 100644 --- a/src/OCPI.Net.Core/Enums/Versioning/OcpiVersion.cs +++ b/src/OCPI.Net.Core/Enums/Versioning/OcpiVersion.cs @@ -2,10 +2,8 @@ namespace OCPI; -public enum OcpiVersion : ushort +public enum OcpiVersion : byte { - Invalid = 0, - [EnumMember(Value = "2.0")] v2_0 = 200,