diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml deleted file mode 100644 index 2e62656..0000000 --- a/.github/dependabot.yaml +++ /dev/null @@ -1,17 +0,0 @@ -version: 2 -updates: - - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - - - package-ecosystem: "nuget" - directory: "/" - schedule: - interval: "weekly" - ignore: - # Target the lowest version of FSharp.Core, for max compat - - dependency-name: "FSharp.Core" - # Target the lowest compatible version of System.Text.Json - - dependency-name: "System.Text.Json" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..44b1223 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,63 @@ +name: Build + +on: + push: + pull_request: + +env: + # Stop wasting time caching packages + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + # Disable sending usage data to Microsoft + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_VERSION: 8.0.301 + +# Kill other jobs when we trigger this workflow by sending new commits +# to the PR. +# https://stackoverflow.com/a/72408109 +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + fantomas-check: + name: "Format with Fantomas" + runs-on: ubuntu-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Tool Restore + run: dotnet tool restore + + - name: Lint + run: dotnet fantomas -r --check . + + build: + name: Build the project + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # workaround for https://github.com/actions/runner/issues/2033 + - name: ownership workaround + run: git config --global --add safe.directory '*' + + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Restore nuget dependencies + run: dotnet restore + + - name: Build + run: dotnet build -c Release --no-restore diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..ffae6b4 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,73 @@ +name: Publish + +on: + push: + pull_request: + release: + types: + - published + +env: + # Stop wasting time caching packages + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + # Disable sending usage data to Microsoft + DOTNET_CLI_TELEMETRY_OPTOUT: true + # Project name to pack and publish + PROJECT_NAME: Giraffe.OpenApi + DOTNET_VERSION: 8.0.301 + # GitHub Packages Feed settings + GITHUB_FEED: https://nuget.pkg.github.com/giraffe-fsharp/ + GITHUB_USER: dustinmoris + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Official NuGet Feed settings + NUGET_FEED: https://api.nuget.org/v3/index.json + NUGET_KEY: ${{ secrets.NUGET_KEY }} + +jobs: + build: + name: Build the project + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build -c Release --no-restore + + deploy: + name: Publish a new version + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Create Release NuGet package + run: | + arrTag=(${GITHUB_REF//\// }) + VERSION="${arrTag[2]}" + echo Version: $VERSION + + VERSION="${VERSION//v}" + echo Clean Version: $VERSION + + dotnet pack -v normal -c Release --include-symbols --include-source -p:PackageVersion=$VERSION -o nupkg src/$PROJECT_NAME/$PROJECT_NAME.*proj + - name: Push to GitHub Feed + run: | + for f in ./nupkg/*.nupkg + do + echo $f + curl -vX PUT -u "$GITHUB_USER:$GITHUB_TOKEN" -F package=@$f $GITHUB_FEED + done + - name: Push to NuGet Feed + run: dotnet nuget push ./nupkg/*.nupkg --source $NUGET_FEED --skip-duplicate --api-key $NUGET_KEY diff --git a/Directory.Build.props b/Directory.Build.props index a6d10d3..a914979 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,26 +1,6 @@ - Moritz Jörg - false - true - Copyright © $([System.DateTime]::UtcNow.Year) - true - true - embedded - OpenApi support for Giraffe - https://github.com/mrtz-j/Giraffe.OpenApi - MIT - https://github.com/mrtz-j/Giraffe.OpenApi/src/Giraffe.OpenApi - git - Giraffe;ASP.NET Core;F#;FSharp;Http;Web;OpenApi - README.md - 0.0.1 - 0.0.1 - Initial Version - - - true true @@ -31,6 +11,7 @@ + all build diff --git a/README.md b/README.md index e348d58..4f07535 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,137 @@ +![Giraffe](https://raw.githubusercontent.com/giraffe-fsharp/Giraffe/master/giraffe.png) + # Giraffe.OpenApi -> [!IMPORTANT] -> This is currently only works with my branch of Giraffe, which can be found [here](https://github.com/mrtz-j/Giraffe/tree/configureEndpoint). +An extension for the [Giraffe](https://github.com/giraffe-fsharp/Giraffe) Web Application framework with functionality to auto generate OpenApi documentation spec from code. + +[![NuGet Info](https://buildstats.info/nuget/Giraffe.OpenApi?includePreReleases=true)](https://www.nuget.org/packages/Giraffe.OpenApi/) + + +## Table of Contents + +- [About](#about) +- [Getting Started](#getting-started) +- [Documentation](#documentation) + - [Integration](#integration) + - [addOpenApi](#addopenapi) + - [addOpenApiSimple](#addopenapisimple) + - [configureEndpoint](#configureendpoint) +- [License](#license) + +## About + +`Giraffe.OpenApi` is a library that extends the `Giraffe` Web Application framework with functionality to auto generate OpenApi documentation spec from code. This means that you can define your API endpoints using Giraffe and generate OpenApi or Swagger documentation from it. + +Inspired by the [Oxpecker.OpenApi](https://github.com/Lanayx/Oxpecker/blob/develop/src/Oxpecker.OpenApi) library, but adapted to work with Giraffe. + +## Getting Started + +Add the `Giraffe.OpenApi` NuGet package to your project: + +```bash +dotnet add package Giraffe.OpenApi +``` + +Two use cases: + +```fsharp +open Giraffe +open Giraffe.EndpointRouting +open Giraffe.OpenApi + +let endpoints = [ + // addOpenApi supports passing detailed configuration + POST [ + route "/product" (text "Product posted!") + |> addOpenApi (OpenApiConfig( + requestBody = RequestBody(typeof), + responseBodies = [| ResponseBody(typeof) |], + configureOperation = (fun o -> o.OperationId <- "PostProduct"; o) + )) + ] + // addOpenApiSimple is a shortcut for simple cases + GET [ + routef "/product/{%i}" ( + fun id -> + forecases + |> Array.find (fun f -> f.Id = num) + |> json + ) + |> configureEndpoint _.WithName("GetProduct") + |> addOpenApiSimple + ] +] +``` + +## Documentation + +### Integration + +Since `Giraffe.OpenApi` works on top of `Microsoft.AspNetCore.OpenApi` and `Swashbuckle.AspNetCore` packages, you need to do [standard steps](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/openapi): + +```fsharp +let configureApp (appBuilder: IApplicationBuilder) = + appBuilder + .UseRouting() + .UseSwagger() // For generating OpenApi spec + .UseSwaggerUI() // For viewing Swagger UI + .UseGiraffe(endpoints) + .UseGiraffe(notFoundHandler) + +let configureServices (services: IServiceCollection) = + services + .AddRouting() + .AddGiraffe() + .AddEndpointsApiExplorer() // Use the API Explorer to discover and describe endpoints + .AddSwaggerGen() // Swagger dependencies + |> ignore +``` + +To make endpoints discoverable by Swagger, you need to call one of the following functions: `addOpenApi` or `addOpenApiSimple` on the endpoint. + +_NOTE: you don't have to describe routing parameters when using those functions, they will be inferred from the route template automatically._ + +### addOpenApi + +This method is used to add OpenApi metadata to the endpoint. It accepts `OpenApiConfig` object with the following optional parameters: + +```fsharp +type OpenApiConfig (?requestBody : RequestBody, + ?responseBodies : ResponseBody seq, + ?configureOperation : OpenApiOperation -> OpenApiOperation) = + // ... +``` + +Response body schema will be inferred from the types passed to `requestBody` and `responseBodies` parameters. Each `ResponseBody` object in sequence must have different status code. + +`configureOperation` parameter is a function that allows you to do very low-level modifications the `OpenApiOperation` object. + +### addOpenApiSimple + +This method is a shortcut for simple cases. It accepts two generic type parameters - request and response, so the schema can be inferred from them. + +```fsharp +let addOpenApiSimple<'Req, 'Res> = ... +``` + +If your handler doesn't accept any input, you can pass `unit` as a request type (works for response as well). + +### configureEndpoint + +The two methods above return `Endpoint` object, which can be further configured using `configureEndpoint` method provided by [Giraffe](https://github.com/giraffe-fsharp/Giraffe). It accepts `Endpoint` object and returns the same object, so you can chain multiple calls. + +```fsharp +let endpoints = [ + GET [ + route "/hello" (text "Hello, World!") + |> configureEndpoint _.WithName("HelloWorld") + |> configureEndpoint _.WithDescription("Simple hello world endpoint") + |> configureEndpoint _.WithSummary("Hello world") + |> addOpenApiSimple + ] +] +``` -Auto generate an API documentation for Giraffe Web Applications using OpenApi +## License +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/build.fsx b/build.fsx index 97cf62d..0f2a43f 100644 --- a/build.fsx +++ b/build.fsx @@ -19,7 +19,7 @@ pipeline "Build" { async { let deleteIfExists folder = if Directory.Exists folder then - Directory.Delete (folder, true) + Directory.Delete(folder, true) deleteIfExists packageOutput deleteIfExists (__SOURCE_DIRECTORY__ "output") @@ -60,11 +60,11 @@ pipeline "Analyze" { runIfOnlySpecified true } - pipeline "Publish" { workingDir __SOURCE_DIRECTORY__ stage "publish" { - run "dotnet publish --nologo -c Release --ucr -p:PublishReadyToRun=true ./src/Giraffe.OpenApi/Giraffe.OpenApi.fsproj" + run + "dotnet publish --nologo -c Release --ucr -p:PublishReadyToRun=true ./src/Giraffe.OpenApi/Giraffe.OpenApi.fsproj" } runIfOnlySpecified true } diff --git a/giraffe-64.png b/giraffe-64.png new file mode 100644 index 0000000..40ed5e3 Binary files /dev/null and b/giraffe-64.png differ diff --git a/sample-project/Program.fs b/sample-project/Program.fs index 614bb5b..0d4cd79 100644 --- a/sample-project/Program.fs +++ b/sample-project/Program.fs @@ -28,17 +28,33 @@ let handler2 (firstName: string, age: int) (_: HttpFunc) (ctx: HttpContext) = let endpoints = [ GET [ route "/hello" (json { Hello = "Hello from Giraffe" }) - |> configureEndpoint _.WithTags("helloGiraffe") + |> configureEndpoint _.WithTags("SampleApp") |> configureEndpoint _.WithSummary("Fetches a Hello from Giraffe") |> configureEndpoint _.WithDescription("Will return a Hello from Giraffe.") |> addOpenApiSimple routef "/%s/%i" handler2 - |> configureEndpoint _.WithTags("handler2") + |> configureEndpoint _.WithTags("SampleApp") |> configureEndpoint _.WithSummary("Fetches a response from handler2") |> configureEndpoint _.WithDescription("Will return a Hello from Handler 2.") |> addOpenApiSimple ] + POST [ + route "/message" (text "Message posted!") + |> configureEndpoint _.WithSummary("Posts a message") + |> configureEndpoint _.WithDescription("Will return a message posted") + |> addOpenApi ( + OpenApiConfig( + requestBody = RequestBody(typeof), + responseBodies = [| ResponseBody(typeof) |], + configureOperation = + (fun o -> + o.OperationId <- "PostMessage" + o + ) + ) + ) + ] ] let notFoundHandler = "Not Found" |> text |> RequestErrors.notFound @@ -52,8 +68,7 @@ let configureApp (appBuilder: IApplicationBuilder) = .UseGiraffe(notFoundHandler) let configureServices (services: IServiceCollection) = - // Configure OpenApi - let openApiInfo = OpenApiInfo() + let openApiInfo = OpenApiInfo() // Configure OpenApi openApiInfo.Description <- "Documentation for my API" openApiInfo.Title <- "My API" openApiInfo.Version <- "v1" diff --git a/sample-project/SampleApp.fsproj b/sample-project/SampleApp.fsproj index 4310664..493447c 100644 --- a/sample-project/SampleApp.fsproj +++ b/sample-project/SampleApp.fsproj @@ -2,6 +2,8 @@ Exe net8.0 + true + false diff --git a/src/Giraffe.OpenApi/Giraffe.OpenApi.fsproj b/src/Giraffe.OpenApi/Giraffe.OpenApi.fsproj index 25eda3f..d550bcd 100644 --- a/src/Giraffe.OpenApi/Giraffe.OpenApi.fsproj +++ b/src/Giraffe.OpenApi/Giraffe.OpenApi.fsproj @@ -1,11 +1,33 @@  - Exe net8.0 - True + Giraffe.OpenApi + Giraffe.OpenApi + Giraffe.OpenApi + Giraffe.OpenApi + OpenApi support for Giraffe + https://github.com/mrtz-j/Giraffe.OpenApi + MIT + https://github.com/mrtz-j/Giraffe.OpenApi/src/Giraffe.OpenApi + giraffe-64.png + git + Giraffe;ASP.NET Core;F#;FSharp;Http;Web;OpenApi + Moritz Jörg, F# Community + README.md + true + snupkg + true + true + true + 0.0.1 + 0.0.1 + Initial Version + Copyright © $([System.DateTime]::UtcNow.Year) + +