Skip to content
Open
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"rollForward": false
},
"fantomas": {
"version": "7.0.0",
"version": "7.0.3",
"commands": [
"fantomas"
],
Expand Down
63 changes: 63 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
## Build, Test & Lint Commands

- **Build**: `dotnet fake build -t Build` (Release configuration)
- **Format Check**: `dotnet fake build -t CheckFormat` (validates Fantomas formatting)
- **Format**: `dotnet fake build -t Format` (applies Fantomas formatting)
- **All Tests**: `dotnet fake build -t RunTests` (builds + starts test server + runs all tests)
- **Unit Tests Only**: `dotnet build && dotnet tests/SwaggerProvider.Tests/bin/Release/net9.0/SwaggerProvider.Tests.dll`
- **Provider Tests (Integration)**:
1. Build test server: `dotnet build tests/Swashbuckle.WebApi.Server/Swashbuckle.WebApi.Server.fsproj -c Release`
2. Start server in background: `dotnet tests/Swashbuckle.WebApi.Server/bin/Release/net9.0/Swashbuckle.WebApi.Server.dll`
3. Build tests: `dotnet build SwaggerProvider.TestsAndDocs.sln -c Release`
4. Run tests: `dotnet tests/SwaggerProvider.ProviderTests/bin/Release/net9.0/SwaggerProvider.ProviderTests.dll`
- **Single Test**: Run via xunit runner: `dotnet [assembly] [filter]`

## Code Style Guidelines

**Language**: F# (net9.0 target framework)

**Imports & Namespaces**:

- `namespace [Module]` at file start; no `open` statements at module level
- Use `module [Name]` for nested modules
- Open dependencies after namespace declaration (e.g., `open Xunit`, `open FsUnitTyped`)
- Fully qualify internal modules: `SwaggerProvider.Internal.v2.Parser`, `SwaggerProvider.Internal.v3.Compilers`

**Formatting** (via Fantomas, EditorConfig enforced):

- 4-space indents, max 150 char line length
- `fsharp_max_function_binding_width=10`, `fsharp_max_infix_operator_expression=70`
- No space before parameter/lowercase invocation
- Multiline block brackets on same column, Stroustrup style enabled
- Bar before discriminated union declarations, max 3 blank lines

**Naming Conventions**:

- PascalCase for classes, types, modules, public members
- camelCase for local/private bindings, parameters
- Suffix test functions with `Tests` or use attributes like `[<Theory>]`, `[<Fact>]`

**Type Annotations**:

- Explicit return types for public functions (recommended)
- Use type inference for local bindings when obvious
- Generic type parameters: `'a`, `'b` (single quote prefix)

**Error Handling**:

- Use `Result<'T, 'Error>` or `Option<'T>` for fallible operations
- `failwith` or `failwithf` for errors in type providers and compilers
- Task-based async for I/O: `task { }` expressions in tests
- Match failures with `| _ -> ...` or pattern guards with `when`

**File Organization**:

- Tests use Xunit attributes: `[<Theory>]`, `[<Fact>]`, `[<MemberData>]`
- Design-time providers in `src/SwaggerProvider.DesignTime/`, runtime in `src/SwaggerProvider.Runtime/`
- Test schemas organized by OpenAPI version: `tests/.../Schemas/{v2,v3}/`

## Key Patterns

- Type Providers use `ProvidedApiClientBase` and compiler pipeline (DefinitionCompiler, OperationCompiler)
- SSRF protection enabled by default; disable with `SsrfProtection=false` static parameter
- Target net9.0; use implicit async/await (task expressions)
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ This SwaggerProvider can be used to access RESTful API generated using [Swagger.

Documentation: http://fsprojects.github.io/SwaggerProvider/

**Security:** SSRF protection is enabled by default. For local development, use static parameter `SsrfProtection=false`.

## Swagger RESTful API Documentation Specification

Swagger is available for ASP.NET WebAPI APIs with [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle).
Expand Down
17 changes: 17 additions & 0 deletions docs/OpenApiClientProvider.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,26 @@ let client = PetStore.Client()
| `IgnoreControllerPrefix` | Do not parse `operationsId` as `<controllerName>_<methodName>` and generate one client class for all operations. Default value `true`. |
| `PreferNullable` | Provide `Nullable<_>` for not required properties, instead of `Option<_>`. Defaults value `false`. |
| `PreferAsync` | Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`. |
| `SsrfProtection` | Enable SSRF protection (blocks HTTP and localhost). Set to `false` for development/testing. Default value `true`. |

More configuration scenarios are described in [Customization section](/Customization)

## Security (SSRF Protection)

By default, SwaggerProvider blocks HTTP URLs and localhost/private IP addresses to prevent [SSRF attacks](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery).

For **development and testing** with local servers, disable SSRF protection:

```fsharp
// Development: Allow HTTP and localhost
type LocalApi = OpenApiClientProvider<"http://localhost:5000/swagger.json", SsrfProtection=false>

// Production: HTTPS with SSRF protection (default)
type ProdApi = OpenApiClientProvider<"https://api.example.com/swagger.json">
```

**Warning:** Never set `SsrfProtection=false` in production code.

## Sample

Sample uses [TaskBuilder.fs](https://github.com/rspeele/TaskBuilder.fs) (F# computation expression builder for System.Threading.Tasks) that will become part of [Fsharp.Core.dll] one day [[WIP, RFC FS-1072] task support](https://github.com/dotnet/fsharp/pull/6811).
Expand Down
17 changes: 17 additions & 0 deletions docs/SwaggerClientProvider.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,26 @@ When you use TP you can specify the following parameters
| `IgnoreControllerPrefix` | Do not parse `operationsId` as `<controllerName>_<methodName>` and generate one client class for all operations. Default value `true`. |
| `PreferNullable` | Provide `Nullable<_>` for not required properties, instead of `Option<_>`. Defaults value `false`. |
| `PreferAsync` | Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`. |
| `SsrfProtection` | Enable SSRF protection (blocks HTTP and localhost). Set to `false` for development/testing. Default value `true`. |

More configuration scenarios are described in [Customization section](/Customization)

## Security (SSRF Protection)

By default, SwaggerProvider blocks HTTP URLs and localhost/private IP addresses to prevent [SSRF attacks](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery).

For **development and testing** with local servers, disable SSRF protection:

```fsharp
// Development: Allow HTTP and localhost
type LocalApi = SwaggerClientProvider<"http://localhost:5000/swagger.json", SsrfProtection=false>

// Production: HTTPS with SSRF protection (default)
type ProdApi = SwaggerClientProvider<"https://api.example.com/swagger.json">
```

**Warning:** Never set `SsrfProtection=false` in production code.

## Sample

The usage is very similar to [OpenApiClientProvider](/OpenApiClientProvider#sample)
Expand Down
30 changes: 16 additions & 14 deletions src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ open Swagger
open SwaggerProvider.Internal
open SwaggerProvider.Internal.v3.Compilers

module Cache =
module OpenApiCache =
let providedTypes = Caching.createInMemoryCache(TimeSpan.FromSeconds 30.0)

/// The Open API Provider.
Expand Down Expand Up @@ -36,36 +36,38 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this =
ProvidedStaticParameter("IgnoreOperationId", typeof<bool>, false)
ProvidedStaticParameter("IgnoreControllerPrefix", typeof<bool>, true)
ProvidedStaticParameter("PreferNullable", typeof<bool>, false)
ProvidedStaticParameter("PreferAsync", typeof<bool>, false) ]
ProvidedStaticParameter("PreferAsync", typeof<bool>, false)
ProvidedStaticParameter("SsrfProtection", typeof<bool>, true) ]

t.AddXmlDoc
"""<summary>Statically typed OpenAPI provider.</summary>
<param name='Schema'>Url or Path to OpenAPI schema file.</param>
<param name='IgnoreOperationId'>Do not use `operationsId` and generate method names using `path` only. Default value `false`.</param>
<param name='IgnoreControllerPrefix'>Do not parse `operationsId` as `<controllerName>_<methodName>` and generate one client class for all operations. Default value `true`.</param>
<param name='PreferNullable'>Provide `Nullable<_>` for not required properties, instead of `Option<_>`. Defaults value `false`.</param>
<param name='PreferAsync'>Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`.</param>"""
<param name='PreferAsync'>Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`.</param>
<param name='SsrfProtection'>Enable SSRF protection (blocks HTTP and localhost). Set to false for development/testing. Default value `true`.</param>"""

t.DefineStaticParameters(
staticParams,
fun typeName args ->
let schemaPath =
let schemaPathRaw = unbox<string> args.[0]
SchemaReader.getAbsolutePath cfg.ResolutionFolder schemaPathRaw

let schemaPathRaw = unbox<string> args.[0]
let ignoreOperationId = unbox<bool> args.[1]
let ignoreControllerPrefix = unbox<bool> args.[2]
let preferNullable = unbox<bool> args.[3]
let preferAsync = unbox<bool> args.[4]
let ssrfProtection = unbox<bool> args.[5]

let cacheKey =
(schemaPath, ignoreOperationId, ignoreControllerPrefix, preferNullable, preferAsync)
(schemaPathRaw, ignoreOperationId, ignoreControllerPrefix, preferNullable, preferAsync, ssrfProtection)
|> sprintf "%A"


let addCache() =
lazy
let schemaData = SchemaReader.readSchemaPath "" schemaPath |> Async.RunSynchronously
let schemaData =
SchemaReader.readSchemaPath (not ssrfProtection) "" cfg.ResolutionFolder schemaPathRaw
|> Async.RunSynchronously

let openApiReader = Microsoft.OpenApi.Readers.OpenApiStringReader()

let (schema, diagnostic) = openApiReader.Read(schemaData)
Expand All @@ -90,18 +92,18 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this =
let ty =
ProvidedTypeDefinition(tempAsm, ns, typeName, Some typeof<obj>, isErased = false, hideObjectMethods = true)

ty.AddXmlDoc("OpenAPI Provider for " + schemaPath)
ty.AddXmlDoc("OpenAPI Provider for " + schemaPathRaw)
ty.AddMembers tys
tempAsm.AddTypes [ ty ]

ty

try
Cache.providedTypes.GetOrAdd(cacheKey, addCache).Value
OpenApiCache.providedTypes.GetOrAdd(cacheKey, addCache).Value
with _ ->
Cache.providedTypes.Remove(cacheKey) |> ignore
OpenApiCache.providedTypes.Remove(cacheKey) |> ignore

Cache.providedTypes.GetOrAdd(cacheKey, addCache).Value
OpenApiCache.providedTypes.GetOrAdd(cacheKey, addCache).Value
)

t
Expand Down
23 changes: 13 additions & 10 deletions src/SwaggerProvider.DesignTime/Provider.SwaggerClient.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ open SwaggerProvider.Internal
open SwaggerProvider.Internal.v2.Parser
open SwaggerProvider.Internal.v2.Compilers

module SwaggerCache =
let providedTypes = Caching.createInMemoryCache(TimeSpan.FromSeconds 30.0)

/// The Swagger Type Provider.
[<TypeProvider; Obsolete("Use OpenApiClientTypeProvider when possible, it supports v2 & v3 schema formats.")>]
type public SwaggerTypeProvider(cfg: TypeProviderConfig) as this =
Expand All @@ -35,7 +38,8 @@ type public SwaggerTypeProvider(cfg: TypeProviderConfig) as this =
ProvidedStaticParameter("IgnoreOperationId", typeof<bool>, false)
ProvidedStaticParameter("IgnoreControllerPrefix", typeof<bool>, true)
ProvidedStaticParameter("PreferNullable", typeof<bool>, false)
ProvidedStaticParameter("PreferAsync", typeof<bool>, false) ]
ProvidedStaticParameter("PreferAsync", typeof<bool>, false)
ProvidedStaticParameter("SsrfProtection", typeof<bool>, true) ]

t.AddXmlDoc
"""<summary>Statically typed Swagger provider.</summary>
Expand All @@ -44,29 +48,28 @@ type public SwaggerTypeProvider(cfg: TypeProviderConfig) as this =
<param name='IgnoreOperationId'>Do not use `operationsId` and generate method names using `path` only. Default value `false`.</param>
<param name='IgnoreControllerPrefix'>Do not parse `operationsId` as `<controllerName>_<methodName>` and generate one client class for all operations. Default value `true`.</param>
<param name='PreferNullable'>Provide `Nullable<_>` for not required properties, instead of `Option<_>`. Defaults value `false`.</param>
<param name='PreferAsync'>Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`.</param>"""
<param name='PreferAsync'>Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`.</param>
<param name='SsrfProtection'>Enable SSRF protection (blocks HTTP and localhost). Set to false for development/testing. Default value `true`.</param>"""

t.DefineStaticParameters(
staticParams,
fun typeName args ->
let schemaPath =
let schemaPathRaw = unbox<string> args.[0]
SchemaReader.getAbsolutePath cfg.ResolutionFolder schemaPathRaw

let schemaPathRaw = unbox<string> args.[0]
let headersStr = unbox<string> args.[1]
let ignoreOperationId = unbox<bool> args.[2]
let ignoreControllerPrefix = unbox<bool> args.[3]
let preferNullable = unbox<bool> args.[4]
let preferAsync = unbox<bool> args.[5]
let ssrfProtection = unbox<bool> args.[6]

let cacheKey =
(schemaPath, headersStr, ignoreOperationId, ignoreControllerPrefix, preferNullable, preferAsync)
(schemaPathRaw, headersStr, ignoreOperationId, ignoreControllerPrefix, preferNullable, preferAsync, ssrfProtection)
|> sprintf "%A"

let addCache() =
lazy
let schemaData =
SchemaReader.readSchemaPath headersStr schemaPath
SchemaReader.readSchemaPath (not ssrfProtection) headersStr cfg.ResolutionFolder schemaPathRaw
|> Async.RunSynchronously

let schema = SwaggerParser.parseSchema schemaData
Expand All @@ -84,13 +87,13 @@ type public SwaggerTypeProvider(cfg: TypeProviderConfig) as this =
let ty =
ProvidedTypeDefinition(tempAsm, ns, typeName, Some typeof<obj>, isErased = false, hideObjectMethods = true)

ty.AddXmlDoc("Swagger Provider for " + schemaPath)
ty.AddXmlDoc("Swagger Provider for " + schemaPathRaw)
ty.AddMembers tys
tempAsm.AddTypes [ ty ]

ty

Cache.providedTypes.GetOrAdd(cacheKey, addCache).Value
SwaggerCache.providedTypes.GetOrAdd(cacheKey, addCache).Value
)

t
Expand Down
Loading
Loading