Add beta server stub generator for F#/Giraffe (#2802)

* Beta server stub generator for F#/Giraffe (#2705)

* first commit for F#/Giraffe

use CLI generator

work on handlers

add binding to url params

add parameter declaration to handler & fix array types

order models by dependency and add tests

add oauth handlers

add service generation

add service implementation

return json for map types and add all return types to service implementation

pare down record types for readability

move implementations to impl folder

fix additional handler invocation

remove logging

remove open api type provider package reference

add sane defaults for OAuth

add readme and reorganize files for easier ignore

fix oauth checks and move login to default template

typedef operation body params as model

add API test templates

fix test templates

set project & other folders when packageName is set

add ignore to test pipes

add ignore for oauth to hide compile warnings

escape model types for generic dictionaries

remove Boolean object from primitives

fix handler and param templates for multiple path params

remove "Model" from model module names and fix import mapping for dictionary

add package name to model imports

change model templates to use imports

move login to CustomHandlers

raise exception where oauth not properly configured

allow webhost configuration from CustomHandlers

remove explicit support for nullable types and render option in template instead

move Cookie options to CustomHandlers

add header params

integrate api key provider

add nullable to datetime types

fix test generation and pretty up model folder

add context path to handler test template

dont copy spec file

remove superseded copyright notices

remove superseded copyright notices

* remove carriage return in fsharp template

* remove superseded sample output directory

* fix bash build script

* update generated sample

* update documentation

* add new file

* fix compile issues
This commit is contained in:
William Cheng 2019-05-06 01:30:51 +08:00 committed by GitHub
parent 3c8d820e3a
commit e8a83dfe1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 5725 additions and 0 deletions

View File

@ -0,0 +1,32 @@
#!/bin/sh
SCRIPT="$0"
echo "# START SCRIPT: $SCRIPT"
while [ -h "$SCRIPT" ] ; do
ls=`ls -ld "$SCRIPT"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
SCRIPT="$link"
else
SCRIPT=`dirname "$SCRIPT"`/"$link"
fi
done
if [ ! -d "${APP_DIR}" ]; then
APP_DIR=`dirname "$SCRIPT"`/..
APP_DIR=`cd "${APP_DIR}"; pwd`
fi
executable="./modules/openapi-generator-cli/target/openapi-generator-cli.jar"
if [ ! -f "$executable" ]
then
mvn clean package
fi
# if you've executed sbt assembly previously it will use that instead.
export JAVA_OPTS="${JAVA_OPTS} -Xmx1024M -DloggerPath=conf/log4j.properties"
ags="generate -i modules/openapi-generator/src/test/resources/3_0/petstore.yaml -t modules/openapi-generator/src/main/resources/fsharp-giraffe-server -g fsharp-giraffe -o samples/server/petstore/fsharp-giraffe $@"
java ${JAVA_OPTS} -jar ${executable} ${ags}

View File

@ -0,0 +1,10 @@
set executable=.\modules\openapi-generator-cli\target\openapi-generator-cli.jar
If Not Exist %executable% (
mvn clean package
)
REM set JAVA_OPTS=%JAVA_OPTS% -Xmx1024M -DloggerPath=conf/log4j.properties
set ags=generate --artifact-id "fsharp-giraffe-petstore-server" -i modules\openapi-generator\src\test\resources\2_0\petstore.yaml -g fsharp-giraffe -o samples\server\petstore\fsharp-giraffe
java %JAVA_OPTS% -jar %executable% %ags%

View File

@ -71,6 +71,7 @@ The following generators are available:
- [cpp-restbed-server](generators/cpp-restbed-server.md)
- [csharp-nancyfx](generators/csharp-nancyfx.md)
- [erlang-server](generators/erlang-server.md)
- [fsharp-giraffe](generators/fsharp-giraffe.md)
- [go-gin-server](generators/go-gin-server.md)
- [go-server](generators/go-server.md)
- [graphql-nodejs-express-server](generators/graphql-nodejs-express-server.md)

View File

@ -0,0 +1,25 @@
---
id: generator-opts-server-fsharp-giraffe
title: Config Options for fsharp-giraffe
sidebar_label: fsharp-giraffe
---
| Option | Description | Values | Default |
| ------ | ----------- | ------ | ------- |
|licenseUrl|The URL of the license| |http://localhost|
|licenseName|The name of the license| |NoLicense|
|packageCopyright|Specifies an AssemblyCopyright for the .NET Framework global assembly attributes stored in the AssemblyInfo file.| |No Copyright|
|packageAuthors|Specifies Authors property in the .NET Core project file.| |OpenAPI|
|packageTitle|Specifies an AssemblyTitle for the .NET Framework global assembly attributes stored in the AssemblyInfo file.| |OpenAPI Library|
|packageName|F# module name (convention: Title.Case).| |OpenAPI|
|packageVersion|F# package version.| |1.0.0|
|packageGuid|The GUID that will be associated with the C# project| |null|
|sourceFolder|source folder for generated code| |OpenAPI/src|
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
|useDateTimeOffset|Use DateTimeOffset to model date-time properties| |false|
|useCollection|Deserialize array types to Collection<T> instead of List<T>.| |false|
|returnICollection|Return ICollection<T> instead of the concrete type.| |false|
|useSwashbuckle|Uses the Swashbuckle.AspNetCore NuGet package for documentation.| |false|
|generateBody|Generates method body.| |true|
|buildTarget|Target the build for a program or library.| |program|

View File

@ -0,0 +1,287 @@
/*
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openapitools.codegen.languages;
import com.samskivert.mustache.Mustache;
import io.swagger.v3.oas.models.OpenAPI;
import sun.reflect.generics.reflectiveObjects.NotImplementedException;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.CodegenOperation;
import org.openapitools.codegen.CodegenType;
import org.openapitools.codegen.SupportingFile;
import org.openapitools.codegen.CodegenModel;
import org.openapitools.codegen.CodegenProperty;
import org.openapitools.codegen.utils.URLPathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.net.URL;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.List;
import java.util.ArrayList;
import static java.util.UUID.randomUUID;
public class FsharpGiraffeServerCodegen extends AbstractFSharpCodegen {
public static final String USE_SWASHBUCKLE = "useSwashbuckle";
public static final String GENERATE_BODY = "generateBody";
public static final String BUILD_TARGET = "buildTarget";
public static final String PROJECT_SDK = "projectSdk";
public static final String SDK_WEB = "Microsoft.NET.Sdk.Web";
public static final String SDK_LIB = "Microsoft.NET.Sdk";
private String packageGuid = "{" + randomUUID().toString().toUpperCase(Locale.ROOT) + "}";
@SuppressWarnings("hiding")
protected Logger LOGGER = LoggerFactory.getLogger(FsharpGiraffeServerCodegen.class);
private boolean useSwashbuckle = false;
protected int serverPort = 8080;
protected String serverHost = "0.0.0.0";
private boolean generateBody = true;
private String buildTarget = "program";
private String projectSdk = SDK_WEB;
public FsharpGiraffeServerCodegen() {
super();
modelPackage = "Model";
apiTemplateFiles.put("Handler.mustache", "Handler.fs");
apiTemplateFiles.put("HandlerParams.mustache", "HandlerParams.fs");
apiTemplateFiles.put("ServiceInterface.mustache", "ServiceInterface.fs");
apiTemplateFiles.put("ServiceImpl.mustache", "Service.fs");
apiTestTemplateFiles.put("HandlerTests.mustache", ".fs");
apiTestTemplateFiles.put("HandlerTestsHelper.mustache", "Helper.fs");
modelTemplateFiles.put("Model.mustache", ".fs");
embeddedTemplateDir = templateDir = "fsharp-giraffe-server";
cliOptions.clear();
// CLI options
addOption(CodegenConstants.LICENSE_URL,
CodegenConstants.LICENSE_URL_DESC,
licenseUrl);
addOption(CodegenConstants.LICENSE_NAME,
CodegenConstants.LICENSE_NAME_DESC,
licenseName);
addOption(CodegenConstants.PACKAGE_COPYRIGHT,
CodegenConstants.PACKAGE_COPYRIGHT_DESC,
packageCopyright);
addOption(CodegenConstants.PACKAGE_AUTHORS,
CodegenConstants.PACKAGE_AUTHORS_DESC,
packageAuthors);
addOption(CodegenConstants.PACKAGE_TITLE,
CodegenConstants.PACKAGE_TITLE_DESC,
packageTitle);
addOption(CodegenConstants.PACKAGE_NAME,
"F# module name (convention: Title.Case).",
packageName);
addOption(CodegenConstants.PACKAGE_VERSION,
"F# package version.",
packageVersion);
addOption(CodegenConstants.OPTIONAL_PROJECT_GUID,
CodegenConstants.OPTIONAL_PROJECT_GUID_DESC,
null);
addOption(CodegenConstants.SOURCE_FOLDER,
CodegenConstants.SOURCE_FOLDER_DESC,
sourceFolder);
// CLI Switches
addSwitch(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG,
CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG_DESC,
sortParamsByRequiredFlag);
addSwitch(CodegenConstants.USE_DATETIME_OFFSET,
CodegenConstants.USE_DATETIME_OFFSET_DESC,
useDateTimeOffsetFlag);
addSwitch(CodegenConstants.USE_COLLECTION,
CodegenConstants.USE_COLLECTION_DESC,
useCollection);
addSwitch(CodegenConstants.RETURN_ICOLLECTION,
CodegenConstants.RETURN_ICOLLECTION_DESC,
returnICollection);
addSwitch(USE_SWASHBUCKLE,
"Uses the Swashbuckle.AspNetCore NuGet package for documentation.",
useSwashbuckle);
addSwitch(GENERATE_BODY,
"Generates method body.",
generateBody);
addOption(BUILD_TARGET,
"Target the build for a program or library.",
buildTarget);
}
@Override
public CodegenType getTag() {
return CodegenType.SERVER;
}
@Override
public String getName() {
return "fsharp-giraffe";
}
@Override
public String getHelp() {
return "Generates a fsharp-giraffe server.";
}
@Override
public void preprocessOpenAPI(OpenAPI openAPI) {
super.preprocessOpenAPI(openAPI);
URL url = URLPathUtils.getServerURL(openAPI);
additionalProperties.put("serverHost", url.getHost());
additionalProperties.put("serverPort", URLPathUtils.getPort(url, 8080));
}
@Override
public void processOpts() {
super.processOpts();
boolean isLibrary = false;
if (additionalProperties.containsKey(CodegenConstants.OPTIONAL_PROJECT_GUID)) {
setPackageGuid((String) additionalProperties.get(CodegenConstants.OPTIONAL_PROJECT_GUID));
}
additionalProperties.put("packageGuid", packageGuid);
if (additionalProperties.containsKey(USE_SWASHBUCKLE)) {
useSwashbuckle = convertPropertyToBooleanAndWriteBack(USE_SWASHBUCKLE);
} else {
additionalProperties.put(USE_SWASHBUCKLE, useSwashbuckle);
}
additionalProperties.put(PROJECT_SDK, projectSdk);
// TODO - should we be supporting a Giraffe class library?
if (isLibrary)
LOGGER.warn("Library flag not currently supported.");
String authFolder = sourceFolder + File.separator + "auth";
String serviceFolder = sourceFolder + File.separator + "services";
String implFolder = sourceFolder + File.separator + "impl";
String helperFolder = sourceFolder + File.separator + "helpers";
supportingFiles.add(new SupportingFile("build.sh.mustache", projectFolder, "build.sh"));
supportingFiles.add(new SupportingFile("build.bat.mustache", projectFolder, "build.bat"));
supportingFiles.add(new SupportingFile("README.mustache", projectFolder, "README.md"));
supportingFiles.add(new SupportingFile("gitignore.mustache", projectFolder, ".gitignore"));
supportingFiles.add(new SupportingFile("Project.fsproj.mustache", sourceFolder, packageName + ".fsproj"));
supportingFiles.add(new SupportingFile("Program.mustache", sourceFolder, "Program.fs"));
supportingFiles.add(new SupportingFile("AuthSchemes.mustache", authFolder, "AuthSchemes.fs"));
supportingFiles.add(new SupportingFile("Helpers.mustache", helperFolder, "Helpers.fs"));
supportingFiles.add(new SupportingFile("CustomHandlers.mustache", implFolder, "CustomHandlers.fs"));
supportingFiles.add(new SupportingFile("Project.Tests.fsproj.mustache",testFolder, packageName + "Tests.fsproj"));
supportingFiles.add(new SupportingFile("TestHelper.mustache",testFolder, "TestHelper.fs"));
// TODO - support Swashbuckle
if (useSwashbuckle)
LOGGER.warn("Swashbuckle flag not currently supported, this will be ignored.");
}
public void setPackageGuid(String packageGuid) {
this.packageGuid = packageGuid;
}
@Override
public String modelFileFolder() {
return super.modelFileFolder().replace("Model","model");
}
@Override
public String apiFileFolder() {
return super.apiFileFolder() + File.separator + "api";
}
private String implFileFolder() {
return outputFolder + File.separator + sourceFolder + File.separator + "impl";
}
@Override()
public String toModelImport(String name) {
return packageName + "." + modelPackage() + "." + name;
}
@Override
public String apiFilename(String templateName, String tag) {
String result = super.apiFilename(templateName, tag);
if (templateName.endsWith("Impl.mustache")) {
int ix = result.lastIndexOf(File.separatorChar);
result = result.substring(0, ix) + result.substring(ix, result.length() - 2) + "fs";
result = result.replace(apiFileFolder(), implFileFolder());
}
return result;
}
@Override
public Map<String, Object> postProcessSupportingFileData(Map<String, Object> objs) {
generateJSONSpecFile(objs);
generateYAMLSpecFile(objs);
return super.postProcessSupportingFileData(objs);
}
@Override
protected void processOperation(CodegenOperation operation) {
super.processOperation(operation);
// HACK: Unlikely in the wild, but we need to clean operation paths for MVC Routing
if (operation.path != null) {
String original = operation.path;
operation.path = operation.path.replace("?", "/");
if (!original.equals(operation.path)) {
LOGGER.warn("Normalized " + original + " to " + operation.path + ". Please verify generated source.");
}
}
// Converts, for example, PUT to HttpPut for controller attributes
operation.httpMethod = "Http" + operation.httpMethod.substring(0, 1) + operation.httpMethod.substring(1).toLowerCase(Locale.ROOT);
}
@Override
public Mustache.Compiler processCompiler(Mustache.Compiler compiler) {
// To avoid unexpected behaviors when options are passed programmatically such as { "useCollection": "" }
return super.processCompiler(compiler).emptyStringIsFalse(true);
}
@Override
public String toRegularExpression(String pattern) {
return escapeText(pattern);
}
}

View File

@ -106,3 +106,4 @@ org.openapitools.codegen.languages.TypeScriptInversifyClientCodegen
org.openapitools.codegen.languages.TypeScriptJqueryClientCodegen
org.openapitools.codegen.languages.TypeScriptNodeClientCodegen
org.openapitools.codegen.languages.TypeScriptRxjsClientCodegen
org.openapitools.codegen.languages.FsharpGiraffeServerCodegen

View File

@ -0,0 +1,100 @@
namespace {{packageName}}
open Microsoft.AspNetCore.Authentication
open Microsoft.AspNetCore.Authentication.Cookies
open Microsoft.Extensions.DependencyInjection
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Authentication.OAuth
open System
open Giraffe
open FSharp.Control.Tasks.V2.ContextInsensitive
open Microsoft.Extensions.Configuration
open AspNet.Security.ApiKey.Providers.Extensions
open AspNet.Security.ApiKey.Providers.Events
module AuthSchemes =
let accessDenied : HttpHandler = setStatusCode 401 >=> text "Access Denied"
let buildGoogle (builder:AuthenticationBuilder) name authorizationUrl scopes (settings:IConfiguration) =
builder.AddGoogle(fun googleOptions -> CustomHandlers.setOAuthOptions "Google" googleOptions scopes settings)
let buildGitHub (builder:AuthenticationBuilder) name authorizationUrl scopes (settings:IConfiguration) =
builder.AddGitHub(fun githubOptions -> CustomHandlers.setOAuthOptions "GitHub" githubOptions scopes settings)
let buildOAuth (builder:AuthenticationBuilder) (name:string) authorizationUrl scopes (settings:IConfiguration) =
builder.AddOAuth(name, (fun (options:OAuthOptions) ->
options.AuthorizationEndpoint <- authorizationUrl
options.TokenEndpoint <- settings.[name + "TokenUrl"]
options.CallbackPath <- PathString(settings.[name + "CallbackPath"])
CustomHandlers.setOAuthOptions "{{name}}" options scopes settings
))
let OAuthBuilders = Map.empty.Add("Google", buildGoogle).Add("GitHub", buildGitHub)
let checkEnvironment (settings:IConfiguration) name =
if (isNull settings.[name + "ClientId"]) then
raise (Exception(name + "ClientId is not set."))
else if (isNull settings.[name + "ClientSecret"]) then
raise (Exception((name + "ClientSecret is not set.")))
let getOAuthBuilder settings name =
// check that "xxxClientId" and "xxxClientSecret" configuration variables have been set for all OAuth providers
checkEnvironment settings name
if OAuthBuilders.ContainsKey(name) then
OAuthBuilders.[name]
else
buildOAuth
let configureOAuth (settings:IConfiguration) services =
{{#authMethods}}
{{#isOAuth}}
(getOAuthBuilder settings "{{name}}") services "{{name}}" "{{authorizationUrl}}" [{{#scopes}}"{{scope}}";{{/scopes}}] settings
{{/isOAuth}}
{{/authMethods}}
let buildApiKeyAuth name (services:AuthenticationBuilder) =
services.AddApiKey(fun options ->
options.Header <- name
options.HeaderKey <- String.Empty
let events = ApiKeyEvents()
options.Events <- CustomHandlers.setApiKeyEvents name events
)
let configureApiKeyAuth (settings:IConfiguration) services =
{{#authMethods}}
{{#isApiKey}}
{{#isKeyInHeader}}
buildApiKeyAuth "{{name}}" services
{{/isKeyInHeader}}
{{^isKeyInHeader}}
raise (NotImplementedException("API key security scheme outside of header has not yet been implemented"))
{{/isKeyInHeader}}
{{/isApiKey}}
{{/authMethods}}
let configureCookie (builder:AuthenticationBuilder) =
builder.AddCookie(CustomHandlers.cookieAuth)
let configureServices (services:IServiceCollection) =
let serviceProvider = services.BuildServiceProvider()
let settings = serviceProvider.GetService<IConfiguration>()
services.AddAuthentication(fun o -> o.DefaultScheme <- CookieAuthenticationDefaults.AuthenticationScheme)
|> configureOAuth settings
|> configureApiKeyAuth settings
|> configureCookie
let (|||) v1 v2 =
match v1 with
| Some v -> v1
| None -> v2
// this can be replaced with ctx.GetCookieValue in Giraffe >= 3.6
let getCookieValue (ctx:HttpContext) (key : string) =
match ctx.Request.Cookies.TryGetValue key with
| true , cookie -> Some cookie
| false, _ -> None

View File

@ -0,0 +1,112 @@
namespace {{packageName}}
open System
open System.Net.Http
open System.Security.Claims
open System.Threading
open Microsoft.AspNetCore
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Http.Features
open Microsoft.AspNetCore.Authentication
open Microsoft.AspNetCore.Authentication.Cookies
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.Logging
open Microsoft.Extensions.DependencyInjection
open FSharp.Control.Tasks.V2.ContextInsensitive
open Giraffe
open Giraffe.GiraffeViewEngine
open Microsoft.AspNetCore.Authentication.OAuth
open System.Threading.Tasks
open AspNet.Security.ApiKey.Providers.Events
module CustomHandlers =
let cookieAuth (o : CookieAuthenticationOptions) =
do
o.Cookie.HttpOnly <- true
o.Cookie.SecurePolicy <- CookieSecurePolicy.SameAsRequest
o.SlidingExpiration <- true
o.ExpireTimeSpan <- TimeSpan.FromDays 7.0
let onCreatingTicket name (ctx:OAuthCreatingTicketContext) =
task {
// implement post-authentication logic for oAuth handlers here
()
} :> Task
let validateApiKey key =
raise (NotImplementedException("API key validation must be implemented"))
let setApiKeyEvents name (events:ApiKeyEvents) =
events.OnApiKeyValidated <- (fun ctx ->
task {
// implement your validation/authentication logic for api key handlers here
if validateApiKey ctx.ApiKey then
// to interact properly with Giraffe's handlers, you will need to manually set the identity
// let claims = ...
// let identity = ClaimsIdentity(claims, ApiKeyDefaults.AuthenticationScheme)
// ctx.HttpContext.User <- ClaimsPrincipal([|identity|])
ctx.Success()
} :> Task
)
events
let setOAuthOptions name (options:OAuthOptions) scopes (settings:IConfiguration) =
options.ClientId <- settings.[name + "ClientId"]
options.ClientSecret <- settings.[name + "ClientSecret"]
for scope in scopes do
options.Scope.Add scope
options.Events.OnCreatingTicket <- Func<OAuthCreatingTicketContext,Tasks.Task>(onCreatingTicket name)
match name with
| "Google" ->
()
| "GitHub" ->
()
| _ ->
()
let logout = signOut CookieAuthenticationDefaults.AuthenticationScheme >=> redirectTo false "/"
let loginView =
html [] [
head [] [
title [] [ str "Welcome" ]
]
body [] [
h1 [] [ str "Welcome" ]
{{#authMethods}}
a [_href "/login-with-{{name}}"] [ str "Login with {{name}}" ]
{{/authMethods}}
]
]
let redirectToLogin : HttpHandler =
htmlView loginView
let handlers : HttpHandler list = [
// insert your handlers here
GET >=>
choose [
route "/login" >=> redirectToLogin
{{#authMethods}}
route "/login-with-{{name}}" >=> challenge "{{name}}"
{{/authMethods}}
route "/logout" >=> logout
]
]
let configureWebHost (builder: IWebHostBuilder) =
// builder
// .UseContentRoot("content")
// .UseWebRoot("static")
builder
let configureApp (app : IApplicationBuilder) =
app
let configureServices (services:IServiceCollection) (authBuilder:AuthenticationBuilder) =
()

View File

@ -0,0 +1,67 @@
namespace {{packageName}}
open System.Collections.Generic
open Giraffe
open Microsoft.AspNetCore.Http
open FSharp.Control.Tasks.V2.ContextInsensitive
open {{classname}}HandlerParams
open {{classname}}ServiceInterface
open {{classname}}ServiceImplementation
{{#imports}}
{{#import}}
open {{import}}
{{/import}}
{{/imports}}
module {{classname}}Handler =
{{#operations}}
/// <summary>
/// {{description}}
/// </summary>
{{#operation}}
//#region {{operationId}}
/// <summary>
/// {{#summary}}{{summary}}{{/summary}}
/// </summary>
let {{operationId}} {{#hasPathParams}}(pathParams:{{operationId}}PathParams){{/hasPathParams}} : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
{{#hasQueryParams}}
let queryParams = ctx.TryBindQueryString<{{operationId}}QueryParams>()
{{/hasQueryParams}}
{{#hasBodyParam}}
let! bodyParams =
ctx.BindJsonAsync<{{operationId}}BodyParams>()
{{/hasBodyParam}}
{{#hasFormParams}}
let! formParams = ctx.TryBindFormAsync<{{operationId}}FormParams>()
{{/hasFormParams}}
{{#hasHeaderParams}}
let headerParams = {
{{#headerParams}}
{{operationId}}HeaderParams.{{paramName}}={{#required}}ctx.GetRequestHeader{{/required}}{{^required}}ctx.TryGetRequestHeader{{/required}} "{{paramName}}";
{{/headerParams}}
}
{{/hasHeaderParams}}
{{#allParams}}
{{#-first}}
let serviceArgs = { {{#hasHeaderParams}}headerParams=headerParams;{{/hasHeaderParams}} {{#hasQueryParams}}queryParams=queryParams;{{/hasQueryParams}} {{#hasFormParams}}formParams=formParams;{{/hasFormParams}} {{#hasPathParams}}pathParams=pathParams;{{/hasPathParams}} {{#hasBodyParam}}bodyParams=bodyParams{{/hasBodyParam}} } : {{operationId}}Args
{{/-first}}
{{/allParams}}
let result = {{classname}}Service.{{operationId}} ctx {{#allParams}}{{#-first}}serviceArgs{{/-first}}{{/allParams}}
return! (match result with
{{#responses}}
| {{operationId}}{{#isDefault}}Default{{/isDefault}}StatusCode{{^isDefault}}{{code}}{{/isDefault}} resolved ->
setStatusCode {{code}} >=> {{#primitiveType}}{{^isMapContainer}}text{{/isMapContainer}}{{/primitiveType}}{{^primitiveType}}json{{/primitiveType}} resolved.content
{{/responses}}
) next ctx
}
//#endregion
{{/operation}}
{{/operations}}

View File

@ -0,0 +1,147 @@
namespace {{packageName}}
{{#imports}}
{{#import}}
open {{import}}
{{/import}}
{{/imports}}
open System.Collections.Generic
open System
module {{classname}}HandlerParams =
{{#operations}}
{{#operation}}
{{#pathParams}}
{{#-first}}
//#region Path parameters
[<CLIMutable>]
type {{operationId}}PathParams = {
{{/-first}}
{{paramName}} : {{dataType}} {{^required}}option{{/required}};
{{#-last}}
}
{{/-last}}
//#endregion
{{/pathParams}}
{{#queryParams}}
{{#-first}}
//#region Query parameters
[<CLIMutable>]
type {{operationId}}QueryParams = {
{{/-first}}
{{paramName}} : {{dataType}} {{^required}}option{{/required}};
{{#-last}}
}
//#endregion
{{/-last}}
{{/queryParams}}
{{#bodyParams}}
{{#-first}}
//#region Body parameters
[<CLIMutable>]
{{^hasMore}}
type {{operationId}}BodyParams = {{dataType}}
{{/hasMore}}
{{#hasMore}}
type {{operationId}}BodyParams = {
{{paramName}} : {{dataType}};
{{/hasMore}}
{{/-first}}
{{^-first}}
{{paramName}} : {{dataType}};
{{/-first}}
{{#-last}}
{{^-first}}
}
{{/-first}}
//#endregion
{{/-last}}
{{/bodyParams}}
{{#formParams}}
//#region Form parameters
{{#-first}}
[<CLIMutable>]
type {{operationId}}FormParams = {
{{/-first}}
{{paramName}} : {{dataType}} {{^required}}option{{/required}};
{{#-last}}
}
{{/-last}}
//#endregion
{{/formParams}}
{{#headerParams}}
//#region Header parameters
{{#-first}}
[<CLIMutable>]
type {{operationId}}HeaderParams = {
{{/-first}}
{{paramName}} : {{dataType}} {{^required}}option{{/required}};
{{#-last}}
}
{{/-last}}
//#endregion
{{/headerParams}}
{{#cookieParams}}
//#region Cookie parameters
{{#-first}}
type {{operationId}}CookieParams = {
{{/-first}}
{{paramName}} : {{dataType}} {{^required}}option{{/required}};
{{#-last}}
}
{{/-last}}
//#endregion
{{/cookieParams}}
{{#responses}}
type {{operationId}}{{#isDefault}}Default{{/isDefault}}StatusCode{{^isDefault}}{{code}}{{/isDefault}}Response = {
content:{{#dataType}}{{{.}}}{{/dataType}}{{^dataType}}string{{/dataType}};
{{^code}}code:int{{/code}}
}
{{/responses}}
type {{operationId}}Result = {{#responses}}{{operationId}}{{#isDefault}}Default{{/isDefault}}StatusCode{{^isDefault}}{{code}}{{/isDefault}} of {{operationId}}{{#isDefault}}Default{{/isDefault}}StatusCode{{^isDefault}}{{code}}{{/isDefault}}Response{{#hasMore}}|{{/hasMore}}{{/responses}}
{{#allParams}}
{{#-first}}
type {{operationId}}Args = {
{{/-first}}
{{/allParams}}
{{#hasHeaderParams}}
headerParams:{{operationId}}HeaderParams;
{{/hasHeaderParams}}
{{#pathParams}}
{{#-first}}
pathParams:{{operationId}}PathParams;
{{/-first}}
{{/pathParams}}
{{#queryParams}}
{{#-first}}
queryParams:Result<{{operationId}}QueryParams,string>;
{{/-first}}
{{/queryParams}}
{{#bodyParams}}
{{#-first}}
bodyParams:{{operationId}}BodyParams
{{/-first}}
{{/bodyParams}}
{{#formParams}}
{{#-first}}
formParams:Result<{{operationId}}FormParams,string>
{{/-first}}
{{/formParams}}
{{#allParams}}
{{#-first}}
}
{{/-first}}
{{/allParams}}
{{/operation}}
{{/operations}}

View File

@ -0,0 +1,65 @@
namespace {{packageName}}.Tests
open System
open System.Net
open System.Net.Http
open System.IO
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.TestHost
open Microsoft.Extensions.DependencyInjection
open FSharp.Control.Tasks.V2.ContextInsensitive
open Xunit
open System.Text
open Newtonsoft
open TestHelper
open {{classname}}HandlerTestsHelper
open {{packageName}}.{{classname}}Handler
open {{packageName}}.{{classname}}HandlerParams
{{#imports}}
open {{import}}
{{/imports}}
module {{classname}}HandlerTests =
// ---------------------------------
// Tests
// ---------------------------------
{{#operations}}
{{#operation}}
{{#responses}}
[<Fact>]
let ``{{operationId}} - {{summary}} returns {{code}} {{#message}}where {{.}}{{/message}}`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "{{contextPath}}{{path}}"{{#pathParams}}.Replace("{{paramName}}", "ADDME"){{/pathParams}}{{#hasQueryParams}} + "?{{#queryParams}}{{paramName}}=ADDME{{#hasMore}}&{{/hasMore}}{{#-last}}"{{/-last}}{{/queryParams}}{{/hasQueryParams}}
{{#hasConsumes}}
// use an example requestBody provided by the spec
let examples = Map.empty{{#consumes}}.Add("{{mediaType}}", get{{operationId}}Example "{{mediaType}}"){{/consumes}}
// or pass a {{#bodyParams}}body of type {{dataType}}{{/bodyParams}}{{#formParams}}form{{/formParams}}
let body = obj() {{#bodyParams}}:?> {{dataType}}{{/bodyParams}} |> Newtonsoft.Json.JsonConvert.SerializeObject |> Encoding.UTF8.GetBytes |> MemoryStream |> StreamContent
body
|> {{httpMethod}} client path
|> isStatus (enum<HttpStatusCode>({{code}}))
|> readText
|> shouldEqual "TESTME"
{{/hasConsumes}}
{{^hasConsumes}}
{{httpMethod}} client path
|> isStatus (enum<HttpStatusCode>({{code}}))
|> readText
|> shouldEqual "TESTME"
|> ignore
{{/hasConsumes}}
}
{{/responses}}
{{/operation}}
{{/operations}}

View File

@ -0,0 +1,46 @@
namespace {{packageName}}.Tests
open System
open System.Net
open System.Net.Http
open System.IO
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.TestHost
open Microsoft.Extensions.DependencyInjection
open FSharp.Control.Tasks.V2.ContextInsensitive
open Xunit
open System.Text
open TestHelper
open {{packageName}}.{{classname}}Handler
open {{packageName}}.{{classname}}HandlerParams
module {{classname}}HandlerTestsHelper =
{{#operations}}
{{#operation}}
{{^consumes}}
()
{{/consumes}}
{{#consumes}}
{{#-first}}
let mutable {{operationId}}Examples = Map.empty
let mutable {{operationId}}Body = ""
{{/-first}}
{{/consumes}}
{{#requestBodyExamples}}
{{operationId}}Body <- WebUtility.HtmlDecode "{{example}}"
{{operationId}}Examples <- {{operationId}}Examples.Add("{{contentType}}", {{operationId}}Body)
{{/requestBodyExamples}}
{{#consumes}}
{{#-first}}
let get{{operationId}}Example mediaType =
{{operationId}}Examples.[mediaType]
|> getConverter mediaType
{{/-first}}
{{/consumes}}
{{/operation}}
{{/operations}}

View File

@ -0,0 +1,12 @@
namespace OpenAPI
module Helpers =
let (>=>) switch1 switch2 =
match switch1 with
| Ok v1 ->
match switch2 with
| Ok v2 ->
Ok(v1, v2)
| Error e -> Error e
| Error e -> Error e

View File

@ -0,0 +1,36 @@
namespace {{packageName}}.{{modelPackage}}
open System
open System.Collections.Generic
{{#imports}}
open {{import}}
{{/imports}}
module {{classname}} =
{{#models}}
{{#model}}
//#region {{classname}}
{{#vars}}
{{#isEnum}}
//#region enums
type {{datatypeWithEnum}} = {{#allowableValues}}{{#enumVars}}{{name}} of {{datatype}} {{^-last}} | {{/-last}} {{/enumVars}}{{/allowableValues}}
//#endregion
{{/isEnum}}
{{/vars}}
type {{name}} = {
{{#vars}}
{{#isEnum}}
{{name}} : {{{datatypeWithEnum}}};
{{/isEnum}}
{{^isEnum}}
{{name}} : {{#isDateTime}}{{^required}}Nullable<{{/required}}{{/isDateTime}}{{{dataType}}}{{#isDateTime}}{{^required}}>{{/required}}{{/isDateTime}};
{{/isEnum}}
{{/vars}}
}
//#endregion
{{/model}}
{{/models}}

View File

@ -0,0 +1,100 @@
namespace {{packageName}}
open System
open System.Net.Http
open System.Security.Claims
open System.Threading
open Microsoft.AspNetCore
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Http.Features
open Microsoft.AspNetCore.Authentication
open Microsoft.AspNetCore.Authentication.Cookies
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.Logging
open Microsoft.Extensions.DependencyInjection
open FSharp.Control.Tasks.V2.ContextInsensitive
open System.Diagnostics
open Giraffe.GiraffeViewEngine
open AspNet.Security.ApiKey.Providers
{{#apiInfo}}
{{#apis}}
open {{classFilename}}HandlerParams
{{/apis}}
{{/apiInfo}}
open Giraffe
module App =
// ---------------------------------
// Error handler
// ---------------------------------
let errorHandler (ex : Exception) (logger : ILogger) =
logger.LogError(EventId(), ex, "An unhandled exception has occurred while executing the request.")
clearResponse >=> setStatusCode 500 >=> text ex.Message
// ---------------------------------
// Web app
// ---------------------------------
let HttpGet = GET
let HttpPost = POST
let HttpPut = PUT
let HttpDelete = DELETE
let authFailure : HttpHandler =
setStatusCode 401 >=> text "You must be authenticated to access this resource."
let webApp =
choose (CustomHandlers.handlers @ [
{{#apiInfo}}
{{#apis}}
{{#operations}}
{{#operation}}
{{httpMethod}} >=> {{^hasPathParams}}route{{/hasPathParams}}{{#hasPathParams}}routeBind<{{operationId}}PathParams>{{/hasPathParams}} "{{contextPath}}{{path}}" {{^pathParams}}>=>{{/pathParams}} {{#pathParams}}(fun x -> {{/pathParams}}{{#authMethods}}{{#isOAuth}}requiresAuthentication authFailure{{/isOAuth}}{{#isApiKey}}challenge ApiKeyDefaults.AuthenticationScheme >=> requiresAuthentication authFailure{{/isApiKey}} >=> {{/authMethods}} {{classname}}Handler.{{operationId}}{{#pathParams}} x){{/pathParams}};
{{/operation}}
{{/operations}}
{{/apis}}
RequestErrors.notFound (text "Not Found")
{{/apiInfo}}
])
// ---------------------------------
// Main
// ---------------------------------
let configureApp (app : IApplicationBuilder) =
app.UseGiraffeErrorHandler(errorHandler)
.UseStaticFiles()
.UseAuthentication()
.UseResponseCaching() |> ignore
CustomHandlers.configureApp app |> ignore
app.UseGiraffe webApp |> ignore
let configureServices (services : IServiceCollection) =
services
.AddResponseCaching()
.AddGiraffe()
|> AuthSchemes.configureServices
|> CustomHandlers.configureServices services
|> ignore
services.AddDataProtection() |> ignore
let configureLogging (loggerBuilder : ILoggingBuilder) =
loggerBuilder.AddFilter(fun lvl -> lvl.Equals LogLevel.Error)
.AddConsole()
.AddDebug() |> ignore
[<EntryPoint>]
let main _ =
let builder = WebHost.CreateDefaultBuilder()
.Configure(Action<IApplicationBuilder> configureApp)
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
|> CustomHandlers.configureWebHost
builder.Build()
.Run()
0

View File

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AssemblyName>{{packageName}}.Tests</AssemblyName>
<DebugType>portable</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.*" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.2.*" />
<PackageReference Include="xunit" Version="2.4.*" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.*" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\{{packageName}}\src\{{packageName}}.fsproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="TestHelper.fs"/>
{{#apiInfo}}
{{#apis}}
<Compile Include="{{classname}}TestsHelper.fs" />
<Compile Include="{{classname}}Tests.fs" />
{{/apis}}
{{/apiInfo}}
</ItemGroup>
</Project>

View File

@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<Description>{{packageName}}</Description>
<Copyright>{{packageName}}</Copyright>
<TargetFramework>netcoreapp2.2</TargetFramework>
<DebugType>portable</DebugType>
<EnableDefaultContentItems>false</EnableDefaultContentItems>
<RunWorkingDirectory>$(MSBuildThisFileDirectory)</RunWorkingDirectory>
<AssemblyName>{{packageName}}</AssemblyName>
<PackageId>{{packageName}}</PackageId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" />
<PackageReference Include="Giraffe" Version="3.4.*" />
<PackageReference Include="TaskBuilder.fs" Version="2.1.*" />
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="2.0.1" />
<PackageReference Include="AspNet.Security.ApiKey.Providers" Version="1.1.0" />
</ItemGroup>
<ItemGroup>
<Content Include="openapi.yaml"/>
<Compile Include="helpers/Helpers.fs" />
{{#models}}
{{#model}}
<Compile Include="model/{{classname}}.fs" />
{{/model}}
{{/models}}
{{#apiInfo}}
{{#apis}}
{{#operations}}
<Compile Include="api/{{classname}}HandlerParams.fs" />
<Compile Include="api/{{classname}}ServiceInterface.fs" />
<Compile Include="impl/{{classname}}Service.fs" />
<Compile Include="api/{{classname}}Handler.fs" />
{{/operations}}
{{/apis}}
{{/apiInfo}}
<Compile Include="impl/CustomHandlers.fs" />
<Compile Include="auth/AuthSchemes.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,198 @@
# {{packageName}}
A [Giraffe](https://github.com/giraffe-fsharp/Giraffe) server stub for the {{packageName}} package, created via the [OpenAPI generator](https://github.com/OpenAPITools/openapi-generator/).
## Models
The following models have been auto-generated from the provided OpenAPI schema:
{{#apiInfo}}
{{#models}}
{{#model}}
- model/{{classname}}Model.fs
{{/model}}
{{/models}}
{{/apiInfo}}
## Operations
Handlers have been auto-generated from the operations specified in the OpenAPI schema as follows:
{{#apiInfo}}
{{#operations}}
{{#operation}}
- api/{{classname}}Handler.fs
{{/operation}}
{{/operations}}
{{/apiInfo}}
## Operation Parameters
Types have been generated for the URL, query, form, header and cookie parameters passed to each handler in the following files:
{{#apiInfo}}
{{#apis}}
- api/{{classname}}HandlerParams.fs
{{/apis}}
{{/apiInfo}}
## Service Interfaces
Handlers will attempt to bind parameters to the applicable type and pass to a Service specific to that Handler. Service interfaces have been generated as follows:
{{#apiInfo}}
{{#apis}}
- api/{{classname}}ServiceInterface.fs
{{/apis}}
{{/apiInfo}}
Each Service contains functions for each [OperationId], each accepting a [OperationId]Params object that wraps the operation's parameters.
If a requestBody is a ref type (i.e. a Model) or a single simple type, the operation parameter will be typed as the expected Model:
`type AddPetBodyParams = Pet`
If a requestBody is a simple type with named properties, the operation parameters will be typed to reflect those properties:
`type AddFooBodyParams = {
Name:string;
Age:int
}
Each Service/operation function must accept the [OperationId]Params object and return a [OperationId]Result type. For example:
`type AddPetArgs = { bodyParams:AddPetBodyParams }
type IPetApiService = abstract member AddPet:HttpContext -> AddPetArgs->AddPetResult`
[OperationId]Result is a discriminated union of all possible OpenAPI response types for that operation.
This means that service implementations can only return status codes that have been declared in the OpenAPI specification.
However, if the OpenAPI spec declares a default Response for an operation, the service can manually set the status code.
For example:
`type FindPetsByStatusDefaultStatusCodeResponse = { content:Pet[];}
type FindPetsByStatusStatusCode400Response = { content:string; }
type FindPetsByStatusResult = FindPetsByStatusDefaultStatusCode of FindPetsByStatusDefaultStatusCodeResponse | FindPetsByStatusStatusCode400 of FindPetsByStatusStatusCode400Response`
## Note on nullable/optional properties
Currently, handler parameters and models do not distinguish between required properties and optional (or nullable) properties***.
If a request body is missing a property, the parameter will be bound as null (and likewise, missing model properties will be serialized as null).
This is only a temporary measure, and does need to be fixed to conform to the OpenAPI spec.
Ideally, Option types would be used for all parameters not marked as required (or marked as nullable).
This won't be possible until Giraffe supports binding option types in request bodies.
This may cause problems with certain parameter types (e.g. map types) - please file an issue if you come across one.
*** Except for DateTime, where properties not marked required are bound as Nullable<DateTime>.
## Note on response codes for URL parameter binding
Giraffe binds URL parameters by requiring compile-time format strings for routes (e.g. "/foo/%s/%d) or known types (e.g. FooUrlParameters).
With either approach, Giraffe will emit a 400 error response if parameter binding fails (e.g. if a string is provided where an int was expected).
Currently, I am not aware of any way to customize this response, meaning if your OpenAPI schema specifies a different response code for an incorrectly formatted URL parameter, this will basically be ignored.
To ensure your OpenAPI schema and implementation are consistent, I suggest ensuring that your schema only specifies return code 400 for incorrectly formatted URL parameters.
If you have any suggestions for customizing this, please file an issue.
## Service Implementations
Stubbed service implementations of those interfaces have been generated as follows:
{{#apiInfo}}
{{#apis}}
- impl/{{classname}}Service.fs
{{/apis}}
{{/apiInfo}}
You should manually edit these files to implement your business logic.
## Additional Handlers
Additional handlers can be configured in the Customization.fs
`let handlers : HttpHandler list = [
// insert your handlers here
GET >=>
choose [
route "/login" >=> redirectToLogin
route "/logout" >=> logout
]
]`
## Authentication
### OAuth
If your OpenAPI spec contains oAuth2 securitySchemes, these will have been auto-generated.
To configure any of these, you must set the "xxxClientId" and "xxxClientSecret" environment variables (e.g. "GoogleClientId", "GoogleClientSecret") where xxx is the securityScheme ID.
If you specify the securityScheme ID as "Google" or "GitHub" (note the capital "G" and "H" in the latter), the generator will default to:
- for Google, the [ASP.NET Core providers](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?view=aspnetcore-2.2)
- for GitHub, the [aspnet-contrib provider](https://www.nuget.org/packages/AspNet.Security.OAuth.GitHub/)
For any other ID (e.g. "Facebook"), a [generic ASP.NET Core oAuth provider](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.oauthextensions.addoauth?view=aspnetcore-2.2) will be configured.
See impl/AuthSchemes.fs for further details.
NOTE - currently, authentication against ANY defined OAuth scheme will allow access to a handler (even if the scheme was not specified as a security scheme for the particular handler).
This is on the TODO list.
### API key
API key authentication is supported via the (AspNet.Security.ApiKey.Providers package)[https://github.com/jamesharling/AspNet.Security.ApiKey.Providers].
You must implement your own validation logic for the key in CustomHandlers.setApiKeyEvents.
## TODO/currently unsupported
- form request bodies (URL-encoded or multipart)
- implicit oAuth
- limit handler access to specified oAuth scheme when multiple oAuth schemes defined
- XML content/response types
- http authentication
- testing header params
## .openapi-generator-ignore
It is recommended to add src/impl/** and the project's .fsproj file to the .openapi-generator-ignore file.
This will allow you to regenerate model, operation and parameter files without overriding your implementations of business logic, authentication, data layers, and so on.
## Build and test the application
### Windows
Run the `build.bat` script in order to restore, build and test (if you've selected to include tests) the application:
```
> ./build.bat
```
### Linux/macOS
Run the `build.sh` script in order to restore, build and test (if you've selected to include tests) the application:
```
$ ./build.sh
```
## Run the application
After a successful build you can start the web application by executing the following command in your terminal:
```
dotnet run --project src/{{packageName}
```
After the application has started visit [http://localhost:5000](http://localhost:5000) in your preferred browser.

View File

@ -0,0 +1,44 @@
namespace {{packageName}}
{{#imports}}
{{#import}}
open {{import}}
{{/import}}
{{/imports}}
open {{classname}}HandlerParams
open {{classname}}ServiceInterface
open System.Collections.Generic
open System
open Giraffe
module {{classname}}ServiceImplementation =
//#region Service implementation
type {{classname}}ServiceImpl() =
interface I{{classname}}Service with
{{#operations}}
{{#operation}}
member this.{{operationId}} ctx {{#allParams}}{{#-first}}args{{/-first}}{{/allParams}} =
{{#responses}}
{{#-first}}
{{#hasMore}}
if true then
{{/hasMore}}
{{/-first}}
{{^-first}}
{{#hasMore}}
else if true then
{{/hasMore}}
{{^hasMore}}
else
{{/hasMore}}
{{/-first}}
let content = "{{message}}" {{#dataType}}:> obj :?> {{{.}}} // this cast is obviously wrong, and is only intended to allow generated project to compile {{/dataType}}
{{operationId}}{{#isDefault}}Default{{/isDefault}}StatusCode{{^isDefault}}{{code}}{{/isDefault}} { content = content }
{{/responses}}
{{/operation}}
{{/operations}}
//#endregion
let {{classname}}Service = {{classname}}ServiceImpl() :> I{{classname}}Service

View File

@ -0,0 +1,17 @@
namespace {{packageName}}
open {{classname}}HandlerParams
open System
open Giraffe
open Microsoft.AspNetCore.Http
module {{classname}}ServiceInterface =
//#region Service interface
type I{{classname}}Service =
{{#operations}}
{{#operation}}
abstract member {{operationId}}:HttpContext {{#allParams}}{{#-first}}-> {{operationId}}Args{{/-first}}{{/allParams}}->{{operationId}}Result
{{/operation}}
{{/operations}}
//#endregion

View File

@ -0,0 +1,83 @@
namespace {{packageName}}.Tests
open System
open System.Net
open System.Net.Http
open System.IO
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.TestHost
open Microsoft.Extensions.DependencyInjection
open FSharp.Control.Tasks.V2.ContextInsensitive
open Xunit
open System.Text
module TestHelper =
// ---------------------------------
// Test server/client setup
// ---------------------------------
let createHost() =
WebHostBuilder()
.UseContentRoot(Directory.GetCurrentDirectory())
.Configure(Action<IApplicationBuilder> {{packageName}}.App.configureApp)
.ConfigureServices(Action<IServiceCollection> {{packageName}}.App.configureServices)
// ---------------------------------
// Helper functions
// ---------------------------------
let HttpGet (client : HttpClient) (path : string) =
client.GetAsync path
|> Async.AwaitTask
|> Async.RunSynchronously
let HttpPost (client: HttpClient) (path : string) content =
client.PostAsync(path, content)
|> Async.AwaitTask
|> Async.RunSynchronously
let HttpPut (client: HttpClient) (path : string) content =
client.PutAsync(path, content)
|> Async.AwaitTask
|> Async.RunSynchronously
let HttpDelete (client: HttpClient) (path : string) =
client.DeleteAsync(path)
|> Async.AwaitTask
|> Async.RunSynchronously
let createRequest (method : HttpMethod) (path : string) =
let url = "http://127.0.0.1" + path
new HttpRequestMessage(method, url)
let addCookiesFromResponse (response : HttpResponseMessage)
(request : HttpRequestMessage) =
request.Headers.Add("Cookie", response.Headers.GetValues("Set-Cookie"))
request
let makeRequest (client : HttpClient) request =
request
|> client.SendAsync
let isStatus (code : HttpStatusCode) (response : HttpResponseMessage) =
Assert.Equal(code, response.StatusCode)
response
let isOfType (contentType : string) (response : HttpResponseMessage) =
Assert.Equal(contentType, response.Content.Headers.ContentType.MediaType)
response
let readText (response : HttpResponseMessage) =
response.Content.ReadAsStringAsync()
|> Async.AwaitTask
|> Async.RunSynchronously
let shouldEqual expected actual =
Assert.Equal(expected, actual)
let getConverter mediaType =
(fun (x:string) ->
match mediaType with
| "application/x-www-form-urlencoded" -> raise (NotSupportedException()) // TODO - implement FormUrlEncodedContent
| _ -> x |> Encoding.UTF8.GetBytes |> MemoryStream |> StreamContent)

View File

@ -0,0 +1,3 @@
dotnet restore src/{{packageName}}.fsproj
dotnet build src/{{packageName}}.fsproj

View File

@ -0,0 +1,4 @@
#!/bin/sh
dotnet restore src/{{packageName}}.fsproj
dotnet build src/{{packageName}}.fsproj

View File

@ -0,0 +1,5 @@
**/node_modules
**/bin/
**/obj/
**/dist/
**/web.config

View File

@ -0,0 +1,94 @@
/*
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openapitools.codegen.csharp;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.languages.AbstractFSharpCodegen;
import org.openapitools.codegen.languages.FsharpGiraffeServerCodegen;
import org.testng.Assert;
import org.testng.annotations.Test;
import com.google.common.collect.Sets;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.parser.util.SchemaTypeUtil;
import org.openapitools.codegen.CodegenModel;
import org.openapitools.codegen.CodegenProperty;
import org.openapitools.codegen.DefaultCodegen;
import org.openapitools.codegen.TestUtils;
import io.swagger.parser.OpenAPIParser;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.parser.core.models.ParseOptions;
import org.openapitools.codegen.MockDefaultGenerator.WrittenTemplateBasedFile;
import java.util.*;
@SuppressWarnings("static-method")
public class FSharpServerCodegenTest {
@Test(description = "sort models according to dependency order")
public void testModelsAreSortedAccordingToDependencyOrder() throws Exception {
final AbstractFSharpCodegen codegen = new P_AbstractFSharpCodegen();
// parent
final CodegenModel parent = new CodegenModel();
CodegenProperty childProp = new CodegenProperty();
childProp.complexType = "child";
childProp.name = "child";
parent.setVars(Collections.singletonList(childProp));
final CodegenModel child = new CodegenModel();
CodegenProperty carProp = new CodegenProperty();
carProp.complexType = "car";
carProp.name = "car";
child.setVars(Collections.singletonList(carProp));
// child
final CodegenModel car = new CodegenModel();
CodegenProperty modelProp = new CodegenProperty();
modelProp.name = "model";
car.setVars(Collections.singletonList(modelProp));
Map<String, Object> models = new HashMap<String,Object>();
models.put("parent", Collections.singletonMap("models", Collections.singletonList(Collections.singletonMap("model", parent))));
models.put("child", Collections.singletonMap("models", Collections.singletonList(Collections.singletonMap("model", child))));
models.put("car", Collections.singletonMap("models", Collections.singletonList(Collections.singletonMap("model", car))));
Map<String,Object> sorted = codegen.postProcessDependencyOrders(models);
Object[] keys = sorted.keySet().toArray();
Assert.assertEquals(keys[0], "car");
Assert.assertEquals(keys[1], "child");
Assert.assertEquals(keys[2], "parent");
}
@Test(description = "modify model imports to explicit set namespace and package name")
public void testModelImportsSpecifyNamespaceAndPackageName() throws Exception {
final AbstractFSharpCodegen codegen = new FsharpGiraffeServerCodegen();
codegen.setPackageName("MyNamespace");
codegen.setModelPackage("Model");
String modified = codegen.toModelImport("Foo");
Assert.assertEquals(modified, "MyNamespace.Model.Foo");
}
private static class P_AbstractFSharpCodegen extends AbstractFSharpCodegen {
}
}

View File

@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md

View File

@ -0,0 +1 @@
4.0.0-SNAPSHOT

View File

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AssemblyName>OpenAPI.Tests</AssemblyName>
<DebugType>portable</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.*" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.2.*" />
<PackageReference Include="xunit" Version="2.4.*" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.*" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OpenAPI\src\OpenAPI.fsproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="TestHelper.fs"/>
<Compile Include="PetApiTestsHelper.fs" />
<Compile Include="PetApiTests.fs" />
<Compile Include="StoreApiTestsHelper.fs" />
<Compile Include="StoreApiTests.fs" />
<Compile Include="UserApiTestsHelper.fs" />
<Compile Include="UserApiTests.fs" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,295 @@
namespace OpenAPI.Tests
open System
open System.Net
open System.Net.Http
open System.IO
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.TestHost
open Microsoft.Extensions.DependencyInjection
open FSharp.Control.Tasks.V2.ContextInsensitive
open Xunit
open System.Text
open Newtonsoft
open TestHelper
open PetApiHandlerTestsHelper
open OpenAPI.PetApiHandler
open OpenAPI.PetApiHandlerParams
open OpenAPI.Model.ApiResponse
open OpenAPI.Model.Pet
module PetApiHandlerTests =
// ---------------------------------
// Tests
// ---------------------------------
[<Fact>]
let ``AddPet - Add a new pet to the store returns 405 where Invalid input`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/pet"
// use an example requestBody provided by the spec
let examples = Map.empty.Add("application/json", getAddPetExample "application/json").Add("application/xml", getAddPetExample "application/xml")
// or pass a body of type Pet
let body = obj() :?> Pet |> Newtonsoft.Json.JsonConvert.SerializeObject |> Encoding.UTF8.GetBytes |> MemoryStream |> StreamContent
body
|> HttpPost client path
|> isStatus (enum<HttpStatusCode>(405))
|> readText
|> shouldEqual "TESTME"
}
[<Fact>]
let ``DeletePet - Deletes a pet returns 400 where Invalid pet value`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/pet/{petId}".Replace("petId", "ADDME")
HttpDelete client path
|> isStatus (enum<HttpStatusCode>(400))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``FindPetsByStatus - Finds Pets by status returns 200 where successful operation`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/pet/findByStatus" + "?status=ADDME"
HttpGet client path
|> isStatus (enum<HttpStatusCode>(200))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``FindPetsByStatus - Finds Pets by status returns 400 where Invalid status value`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/pet/findByStatus" + "?status=ADDME"
HttpGet client path
|> isStatus (enum<HttpStatusCode>(400))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``FindPetsByTags - Finds Pets by tags returns 200 where successful operation`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/pet/findByTags" + "?tags=ADDME&maxCount=ADDME"
HttpGet client path
|> isStatus (enum<HttpStatusCode>(200))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``FindPetsByTags - Finds Pets by tags returns 400 where Invalid tag value`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/pet/findByTags" + "?tags=ADDME&maxCount=ADDME"
HttpGet client path
|> isStatus (enum<HttpStatusCode>(400))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``GetPetById - Find pet by ID returns 200 where successful operation`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/pet/{petId}".Replace("petId", "ADDME")
HttpGet client path
|> isStatus (enum<HttpStatusCode>(200))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``GetPetById - Find pet by ID returns 400 where Invalid ID supplied`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/pet/{petId}".Replace("petId", "ADDME")
HttpGet client path
|> isStatus (enum<HttpStatusCode>(400))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``GetPetById - Find pet by ID returns 404 where Pet not found`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/pet/{petId}".Replace("petId", "ADDME")
HttpGet client path
|> isStatus (enum<HttpStatusCode>(404))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``UpdatePet - Update an existing pet returns 400 where Invalid ID supplied`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/pet"
// use an example requestBody provided by the spec
let examples = Map.empty.Add("application/json", getUpdatePetExample "application/json").Add("application/xml", getUpdatePetExample "application/xml")
// or pass a body of type Pet
let body = obj() :?> Pet |> Newtonsoft.Json.JsonConvert.SerializeObject |> Encoding.UTF8.GetBytes |> MemoryStream |> StreamContent
body
|> HttpPut client path
|> isStatus (enum<HttpStatusCode>(400))
|> readText
|> shouldEqual "TESTME"
}
[<Fact>]
let ``UpdatePet - Update an existing pet returns 404 where Pet not found`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/pet"
// use an example requestBody provided by the spec
let examples = Map.empty.Add("application/json", getUpdatePetExample "application/json").Add("application/xml", getUpdatePetExample "application/xml")
// or pass a body of type Pet
let body = obj() :?> Pet |> Newtonsoft.Json.JsonConvert.SerializeObject |> Encoding.UTF8.GetBytes |> MemoryStream |> StreamContent
body
|> HttpPut client path
|> isStatus (enum<HttpStatusCode>(404))
|> readText
|> shouldEqual "TESTME"
}
[<Fact>]
let ``UpdatePet - Update an existing pet returns 405 where Validation exception`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/pet"
// use an example requestBody provided by the spec
let examples = Map.empty.Add("application/json", getUpdatePetExample "application/json").Add("application/xml", getUpdatePetExample "application/xml")
// or pass a body of type Pet
let body = obj() :?> Pet |> Newtonsoft.Json.JsonConvert.SerializeObject |> Encoding.UTF8.GetBytes |> MemoryStream |> StreamContent
body
|> HttpPut client path
|> isStatus (enum<HttpStatusCode>(405))
|> readText
|> shouldEqual "TESTME"
}
[<Fact>]
let ``UpdatePetWithForm - Updates a pet in the store with form data returns 405 where Invalid input`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/pet/{petId}".Replace("petId", "ADDME")
// use an example requestBody provided by the spec
let examples = Map.empty.Add("application/x-www-form-urlencoded", getUpdatePetWithFormExample "application/x-www-form-urlencoded")
// or pass a formform
let body = obj() |> Newtonsoft.Json.JsonConvert.SerializeObject |> Encoding.UTF8.GetBytes |> MemoryStream |> StreamContent
body
|> HttpPost client path
|> isStatus (enum<HttpStatusCode>(405))
|> readText
|> shouldEqual "TESTME"
}
[<Fact>]
let ``UploadFile - uploads an image returns 200 where successful operation`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/pet/{petId}/uploadImage".Replace("petId", "ADDME")
// use an example requestBody provided by the spec
let examples = Map.empty.Add("multipart/form-data", getUploadFileExample "multipart/form-data")
// or pass a formform
let body = obj() |> Newtonsoft.Json.JsonConvert.SerializeObject |> Encoding.UTF8.GetBytes |> MemoryStream |> StreamContent
body
|> HttpPost client path
|> isStatus (enum<HttpStatusCode>(200))
|> readText
|> shouldEqual "TESTME"
}

View File

@ -0,0 +1,117 @@
namespace OpenAPI.Tests
open System
open System.Net
open System.Net.Http
open System.IO
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.TestHost
open Microsoft.Extensions.DependencyInjection
open FSharp.Control.Tasks.V2.ContextInsensitive
open Xunit
open System.Text
open TestHelper
open OpenAPI.PetApiHandler
open OpenAPI.PetApiHandlerParams
module PetApiHandlerTestsHelper =
let mutable AddPetExamples = Map.empty
let mutable AddPetBody = ""
AddPetBody <- WebUtility.HtmlDecode "{
&quot;photoUrls&quot; : [ &quot;photoUrls&quot;, &quot;photoUrls&quot; ],
&quot;name&quot; : &quot;doggie&quot;,
&quot;id&quot; : 0,
&quot;category&quot; : {
&quot;name&quot; : &quot;name&quot;,
&quot;id&quot; : 6
},
&quot;tags&quot; : [ {
&quot;name&quot; : &quot;name&quot;,
&quot;id&quot; : 1
}, {
&quot;name&quot; : &quot;name&quot;,
&quot;id&quot; : 1
} ],
&quot;status&quot; : &quot;available&quot;
}"
AddPetExamples <- AddPetExamples.Add("application/json", AddPetBody)
AddPetBody <- WebUtility.HtmlDecode "&lt;Pet&gt;
&lt;id&gt;123456789&lt;/id&gt;
&lt;name&gt;doggie&lt;/name&gt;
&lt;photoUrls&gt;
&lt;photoUrls&gt;aeiou&lt;/photoUrls&gt;
&lt;/photoUrls&gt;
&lt;tags&gt;
&lt;/tags&gt;
&lt;status&gt;aeiou&lt;/status&gt;
&lt;/Pet&gt;"
AddPetExamples <- AddPetExamples.Add("application/xml", AddPetBody)
let getAddPetExample mediaType =
AddPetExamples.[mediaType]
|> getConverter mediaType
()
()
()
()
let mutable UpdatePetExamples = Map.empty
let mutable UpdatePetBody = ""
UpdatePetBody <- WebUtility.HtmlDecode "{
&quot;photoUrls&quot; : [ &quot;photoUrls&quot;, &quot;photoUrls&quot; ],
&quot;name&quot; : &quot;doggie&quot;,
&quot;id&quot; : 0,
&quot;category&quot; : {
&quot;name&quot; : &quot;name&quot;,
&quot;id&quot; : 6
},
&quot;tags&quot; : [ {
&quot;name&quot; : &quot;name&quot;,
&quot;id&quot; : 1
}, {
&quot;name&quot; : &quot;name&quot;,
&quot;id&quot; : 1
} ],
&quot;status&quot; : &quot;available&quot;
}"
UpdatePetExamples <- UpdatePetExamples.Add("application/json", UpdatePetBody)
UpdatePetBody <- WebUtility.HtmlDecode "&lt;Pet&gt;
&lt;id&gt;123456789&lt;/id&gt;
&lt;name&gt;doggie&lt;/name&gt;
&lt;photoUrls&gt;
&lt;photoUrls&gt;aeiou&lt;/photoUrls&gt;
&lt;/photoUrls&gt;
&lt;tags&gt;
&lt;/tags&gt;
&lt;status&gt;aeiou&lt;/status&gt;
&lt;/Pet&gt;"
UpdatePetExamples <- UpdatePetExamples.Add("application/xml", UpdatePetBody)
let getUpdatePetExample mediaType =
UpdatePetExamples.[mediaType]
|> getConverter mediaType
let mutable UpdatePetWithFormExamples = Map.empty
let mutable UpdatePetWithFormBody = ""
let getUpdatePetWithFormExample mediaType =
UpdatePetWithFormExamples.[mediaType]
|> getConverter mediaType
let mutable UploadFileExamples = Map.empty
let mutable UploadFileBody = ""
let getUploadFileExample mediaType =
UploadFileExamples.[mediaType]
|> getConverter mediaType

View File

@ -0,0 +1,173 @@
namespace OpenAPI.Tests
open System
open System.Net
open System.Net.Http
open System.IO
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.TestHost
open Microsoft.Extensions.DependencyInjection
open FSharp.Control.Tasks.V2.ContextInsensitive
open Xunit
open System.Text
open Newtonsoft
open TestHelper
open StoreApiHandlerTestsHelper
open OpenAPI.StoreApiHandler
open OpenAPI.StoreApiHandlerParams
open System.Collections.Generic
open OpenAPI.Model.Order
module StoreApiHandlerTests =
// ---------------------------------
// Tests
// ---------------------------------
[<Fact>]
let ``DeleteOrder - Delete purchase order by ID returns 400 where Invalid ID supplied`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/store/order/{orderId}".Replace("orderId", "ADDME")
HttpDelete client path
|> isStatus (enum<HttpStatusCode>(400))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``DeleteOrder - Delete purchase order by ID returns 404 where Order not found`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/store/order/{orderId}".Replace("orderId", "ADDME")
HttpDelete client path
|> isStatus (enum<HttpStatusCode>(404))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``GetInventory - Returns pet inventories by status returns 200 where successful operation`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/store/inventory"
HttpGet client path
|> isStatus (enum<HttpStatusCode>(200))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``GetOrderById - Find purchase order by ID returns 200 where successful operation`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/store/order/{orderId}".Replace("orderId", "ADDME")
HttpGet client path
|> isStatus (enum<HttpStatusCode>(200))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``GetOrderById - Find purchase order by ID returns 400 where Invalid ID supplied`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/store/order/{orderId}".Replace("orderId", "ADDME")
HttpGet client path
|> isStatus (enum<HttpStatusCode>(400))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``GetOrderById - Find purchase order by ID returns 404 where Order not found`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/store/order/{orderId}".Replace("orderId", "ADDME")
HttpGet client path
|> isStatus (enum<HttpStatusCode>(404))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``PlaceOrder - Place an order for a pet returns 200 where successful operation`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/store/order"
// use an example requestBody provided by the spec
let examples = Map.empty.Add("application/json", getPlaceOrderExample "application/json")
// or pass a body of type Order
let body = obj() :?> Order |> Newtonsoft.Json.JsonConvert.SerializeObject |> Encoding.UTF8.GetBytes |> MemoryStream |> StreamContent
body
|> HttpPost client path
|> isStatus (enum<HttpStatusCode>(200))
|> readText
|> shouldEqual "TESTME"
}
[<Fact>]
let ``PlaceOrder - Place an order for a pet returns 400 where Invalid Order`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/store/order"
// use an example requestBody provided by the spec
let examples = Map.empty.Add("application/json", getPlaceOrderExample "application/json")
// or pass a body of type Order
let body = obj() :?> Order |> Newtonsoft.Json.JsonConvert.SerializeObject |> Encoding.UTF8.GetBytes |> MemoryStream |> StreamContent
body
|> HttpPost client path
|> isStatus (enum<HttpStatusCode>(400))
|> readText
|> shouldEqual "TESTME"
}

View File

@ -0,0 +1,42 @@
namespace OpenAPI.Tests
open System
open System.Net
open System.Net.Http
open System.IO
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.TestHost
open Microsoft.Extensions.DependencyInjection
open FSharp.Control.Tasks.V2.ContextInsensitive
open Xunit
open System.Text
open TestHelper
open OpenAPI.StoreApiHandler
open OpenAPI.StoreApiHandlerParams
module StoreApiHandlerTestsHelper =
()
()
()
let mutable PlaceOrderExamples = Map.empty
let mutable PlaceOrderBody = ""
PlaceOrderBody <- WebUtility.HtmlDecode "{
&quot;petId&quot; : 6,
&quot;quantity&quot; : 1,
&quot;id&quot; : 0,
&quot;shipDate&quot; : &quot;2000-01-23T04:56:07.000+00:00&quot;,
&quot;complete&quot; : false,
&quot;status&quot; : &quot;placed&quot;
}"
PlaceOrderExamples <- PlaceOrderExamples.Add("application/json", PlaceOrderBody)
let getPlaceOrderExample mediaType =
PlaceOrderExamples.[mediaType]
|> getConverter mediaType

View File

@ -0,0 +1,83 @@
namespace OpenAPI.Tests
open System
open System.Net
open System.Net.Http
open System.IO
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.TestHost
open Microsoft.Extensions.DependencyInjection
open FSharp.Control.Tasks.V2.ContextInsensitive
open Xunit
open System.Text
module TestHelper =
// ---------------------------------
// Test server/client setup
// ---------------------------------
let createHost() =
WebHostBuilder()
.UseContentRoot(Directory.GetCurrentDirectory())
.Configure(Action<IApplicationBuilder> OpenAPI.App.configureApp)
.ConfigureServices(Action<IServiceCollection> OpenAPI.App.configureServices)
// ---------------------------------
// Helper functions
// ---------------------------------
let HttpGet (client : HttpClient) (path : string) =
client.GetAsync path
|> Async.AwaitTask
|> Async.RunSynchronously
let HttpPost (client: HttpClient) (path : string) content =
client.PostAsync(path, content)
|> Async.AwaitTask
|> Async.RunSynchronously
let HttpPut (client: HttpClient) (path : string) content =
client.PutAsync(path, content)
|> Async.AwaitTask
|> Async.RunSynchronously
let HttpDelete (client: HttpClient) (path : string) =
client.DeleteAsync(path)
|> Async.AwaitTask
|> Async.RunSynchronously
let createRequest (method : HttpMethod) (path : string) =
let url = "http://127.0.0.1" + path
new HttpRequestMessage(method, url)
let addCookiesFromResponse (response : HttpResponseMessage)
(request : HttpRequestMessage) =
request.Headers.Add("Cookie", response.Headers.GetValues("Set-Cookie"))
request
let makeRequest (client : HttpClient) request =
request
|> client.SendAsync
let isStatus (code : HttpStatusCode) (response : HttpResponseMessage) =
Assert.Equal(code, response.StatusCode)
response
let isOfType (contentType : string) (response : HttpResponseMessage) =
Assert.Equal(contentType, response.Content.Headers.ContentType.MediaType)
response
let readText (response : HttpResponseMessage) =
response.Content.ReadAsStringAsync()
|> Async.AwaitTask
|> Async.RunSynchronously
let shouldEqual expected actual =
Assert.Equal(expected, actual)
let getConverter mediaType =
(fun (x:string) ->
match mediaType with
| "application/x-www-form-urlencoded" -> raise (NotSupportedException()) // TODO - implement FormUrlEncodedContent
| _ -> x |> Encoding.UTF8.GetBytes |> MemoryStream |> StreamContent)

View File

@ -0,0 +1,272 @@
namespace OpenAPI.Tests
open System
open System.Net
open System.Net.Http
open System.IO
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.TestHost
open Microsoft.Extensions.DependencyInjection
open FSharp.Control.Tasks.V2.ContextInsensitive
open Xunit
open System.Text
open Newtonsoft
open TestHelper
open UserApiHandlerTestsHelper
open OpenAPI.UserApiHandler
open OpenAPI.UserApiHandlerParams
open OpenAPI.Model.User
module UserApiHandlerTests =
// ---------------------------------
// Tests
// ---------------------------------
[<Fact>]
let ``CreateUser - Create user returns 0 where successful operation`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/user"
// use an example requestBody provided by the spec
let examples = Map.empty.Add("application/json", getCreateUserExample "application/json")
// or pass a body of type User
let body = obj() :?> User |> Newtonsoft.Json.JsonConvert.SerializeObject |> Encoding.UTF8.GetBytes |> MemoryStream |> StreamContent
body
|> HttpPost client path
|> isStatus (enum<HttpStatusCode>(0))
|> readText
|> shouldEqual "TESTME"
}
[<Fact>]
let ``CreateUsersWithArrayInput - Creates list of users with given input array returns 0 where successful operation`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/user/createWithArray"
// use an example requestBody provided by the spec
let examples = Map.empty.Add("application/json", getCreateUsersWithArrayInputExample "application/json")
// or pass a body of type User[]
let body = obj() :?> User[] |> Newtonsoft.Json.JsonConvert.SerializeObject |> Encoding.UTF8.GetBytes |> MemoryStream |> StreamContent
body
|> HttpPost client path
|> isStatus (enum<HttpStatusCode>(0))
|> readText
|> shouldEqual "TESTME"
}
[<Fact>]
let ``CreateUsersWithListInput - Creates list of users with given input array returns 0 where successful operation`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/user/createWithList"
// use an example requestBody provided by the spec
let examples = Map.empty.Add("application/json", getCreateUsersWithListInputExample "application/json")
// or pass a body of type User[]
let body = obj() :?> User[] |> Newtonsoft.Json.JsonConvert.SerializeObject |> Encoding.UTF8.GetBytes |> MemoryStream |> StreamContent
body
|> HttpPost client path
|> isStatus (enum<HttpStatusCode>(0))
|> readText
|> shouldEqual "TESTME"
}
[<Fact>]
let ``DeleteUser - Delete user returns 400 where Invalid username supplied`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/user/{username}".Replace("username", "ADDME")
HttpDelete client path
|> isStatus (enum<HttpStatusCode>(400))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``DeleteUser - Delete user returns 404 where User not found`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/user/{username}".Replace("username", "ADDME")
HttpDelete client path
|> isStatus (enum<HttpStatusCode>(404))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``GetUserByName - Get user by user name returns 200 where successful operation`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/user/{username}".Replace("username", "ADDME")
HttpGet client path
|> isStatus (enum<HttpStatusCode>(200))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``GetUserByName - Get user by user name returns 400 where Invalid username supplied`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/user/{username}".Replace("username", "ADDME")
HttpGet client path
|> isStatus (enum<HttpStatusCode>(400))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``GetUserByName - Get user by user name returns 404 where User not found`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/user/{username}".Replace("username", "ADDME")
HttpGet client path
|> isStatus (enum<HttpStatusCode>(404))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``LoginUser - Logs user into the system returns 200 where successful operation`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/user/login" + "?username=ADDME&password=ADDME"
HttpGet client path
|> isStatus (enum<HttpStatusCode>(200))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``LoginUser - Logs user into the system returns 400 where Invalid username/password supplied`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/user/login" + "?username=ADDME&password=ADDME"
HttpGet client path
|> isStatus (enum<HttpStatusCode>(400))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``LogoutUser - Logs out current logged in user session returns 0 where successful operation`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/user/logout"
HttpGet client path
|> isStatus (enum<HttpStatusCode>(0))
|> readText
|> shouldEqual "TESTME"
|> ignore
}
[<Fact>]
let ``UpdateUser - Updated user returns 400 where Invalid user supplied`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/user/{username}".Replace("username", "ADDME")
// use an example requestBody provided by the spec
let examples = Map.empty.Add("application/json", getUpdateUserExample "application/json")
// or pass a body of type User
let body = obj() :?> User |> Newtonsoft.Json.JsonConvert.SerializeObject |> Encoding.UTF8.GetBytes |> MemoryStream |> StreamContent
body
|> HttpPut client path
|> isStatus (enum<HttpStatusCode>(400))
|> readText
|> shouldEqual "TESTME"
}
[<Fact>]
let ``UpdateUser - Updated user returns 404 where User not found`` () =
task {
use server = new TestServer(createHost())
use client = server.CreateClient()
// add your setup code here
let path = "/v2/user/{username}".Replace("username", "ADDME")
// use an example requestBody provided by the spec
let examples = Map.empty.Add("application/json", getUpdateUserExample "application/json")
// or pass a body of type User
let body = obj() :?> User |> Newtonsoft.Json.JsonConvert.SerializeObject |> Encoding.UTF8.GetBytes |> MemoryStream |> StreamContent
body
|> HttpPut client path
|> isStatus (enum<HttpStatusCode>(404))
|> readText
|> shouldEqual "TESTME"
}

View File

@ -0,0 +1,85 @@
namespace OpenAPI.Tests
open System
open System.Net
open System.Net.Http
open System.IO
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.TestHost
open Microsoft.Extensions.DependencyInjection
open FSharp.Control.Tasks.V2.ContextInsensitive
open Xunit
open System.Text
open TestHelper
open OpenAPI.UserApiHandler
open OpenAPI.UserApiHandlerParams
module UserApiHandlerTestsHelper =
let mutable CreateUserExamples = Map.empty
let mutable CreateUserBody = ""
CreateUserBody <- WebUtility.HtmlDecode "{
&quot;firstName&quot; : &quot;firstName&quot;,
&quot;lastName&quot; : &quot;lastName&quot;,
&quot;password&quot; : &quot;password&quot;,
&quot;userStatus&quot; : 6,
&quot;phone&quot; : &quot;phone&quot;,
&quot;id&quot; : 0,
&quot;email&quot; : &quot;email&quot;,
&quot;username&quot; : &quot;username&quot;
}"
CreateUserExamples <- CreateUserExamples.Add("application/json", CreateUserBody)
let getCreateUserExample mediaType =
CreateUserExamples.[mediaType]
|> getConverter mediaType
let mutable CreateUsersWithArrayInputExamples = Map.empty
let mutable CreateUsersWithArrayInputBody = ""
CreateUsersWithArrayInputBody <- WebUtility.HtmlDecode ""
CreateUsersWithArrayInputExamples <- CreateUsersWithArrayInputExamples.Add("", CreateUsersWithArrayInputBody)
let getCreateUsersWithArrayInputExample mediaType =
CreateUsersWithArrayInputExamples.[mediaType]
|> getConverter mediaType
let mutable CreateUsersWithListInputExamples = Map.empty
let mutable CreateUsersWithListInputBody = ""
CreateUsersWithListInputBody <- WebUtility.HtmlDecode ""
CreateUsersWithListInputExamples <- CreateUsersWithListInputExamples.Add("", CreateUsersWithListInputBody)
let getCreateUsersWithListInputExample mediaType =
CreateUsersWithListInputExamples.[mediaType]
|> getConverter mediaType
()
()
()
()
let mutable UpdateUserExamples = Map.empty
let mutable UpdateUserBody = ""
UpdateUserBody <- WebUtility.HtmlDecode "{
&quot;firstName&quot; : &quot;firstName&quot;,
&quot;lastName&quot; : &quot;lastName&quot;,
&quot;password&quot; : &quot;password&quot;,
&quot;userStatus&quot; : 6,
&quot;phone&quot; : &quot;phone&quot;,
&quot;id&quot; : 0,
&quot;email&quot; : &quot;email&quot;,
&quot;username&quot; : &quot;username&quot;
}"
UpdateUserExamples <- UpdateUserExamples.Add("application/json", UpdateUserBody)
let getUpdateUserExample mediaType =
UpdateUserExamples.[mediaType]
|> getConverter mediaType

View File

@ -0,0 +1,5 @@
**/node_modules
**/bin/
**/obj/
**/dist/
**/web.config

View File

@ -0,0 +1,186 @@
# OpenAPI
A [Giraffe](https://github.com/giraffe-fsharp/Giraffe) server stub for the OpenAPI package, created via the [OpenAPI generator](https://github.com/OpenAPITools/openapi-generator/).
## Models
The following models have been auto-generated from the provided OpenAPI schema:
- model/ApiResponseModel.fs
- model/UserModel.fs
- model/TagModel.fs
- model/CategoryModel.fs
- model/OrderModel.fs
- model/InlineObject1Model.fs
- model/InlineObjectModel.fs
- model/PetModel.fs
## Operations
Handlers have been auto-generated from the operations specified in the OpenAPI schema as follows:
## Operation Parameters
Types have been generated for the URL, query, form, header and cookie parameters passed to each handler in the following files:
- api/PetApiHandlerParams.fs
- api/StoreApiHandlerParams.fs
- api/UserApiHandlerParams.fs
## Service Interfaces
Handlers will attempt to bind parameters to the applicable type and pass to a Service specific to that Handler. Service interfaces have been generated as follows:
- api/PetApiServiceInterface.fs
- api/StoreApiServiceInterface.fs
- api/UserApiServiceInterface.fs
Each Service contains functions for each [OperationId], each accepting a [OperationId]Params object that wraps the operation's parameters.
If a requestBody is a ref type (i.e. a Model) or a single simple type, the operation parameter will be typed as the expected Model:
`type AddPetBodyParams = Pet`
If a requestBody is a simple type with named properties, the operation parameters will be typed to reflect those properties:
`type AddFooBodyParams = {
Name:string;
Age:int
}
Each Service/operation function must accept the [OperationId]Params object and return a [OperationId]Result type. For example:
`type AddPetArgs = { bodyParams:AddPetBodyParams }
type IPetApiService = abstract member AddPet:HttpContext -> AddPetArgs->AddPetResult`
[OperationId]Result is a discriminated union of all possible OpenAPI response types for that operation.
This means that service implementations can only return status codes that have been declared in the OpenAPI specification.
However, if the OpenAPI spec declares a default Response for an operation, the service can manually set the status code.
For example:
`type FindPetsByStatusDefaultStatusCodeResponse = { content:Pet[];}
type FindPetsByStatusStatusCode400Response = { content:string; }
type FindPetsByStatusResult = FindPetsByStatusDefaultStatusCode of FindPetsByStatusDefaultStatusCodeResponse | FindPetsByStatusStatusCode400 of FindPetsByStatusStatusCode400Response`
## Note on nullable/optional properties
Currently, handler parameters and models do not distinguish between required properties and optional (or nullable) properties***.
If a request body is missing a property, the parameter will be bound as null (and likewise, missing model properties will be serialized as null).
This is only a temporary measure, and does need to be fixed to conform to the OpenAPI spec.
Ideally, Option types would be used for all parameters not marked as required (or marked as nullable).
This won't be possible until Giraffe supports binding option types in request bodies.
This may cause problems with certain parameter types (e.g. map types) - please file an issue if you come across one.
*** Except for DateTime, where properties not marked required are bound as Nullable<DateTime>.
## Note on response codes for URL parameter binding
Giraffe binds URL parameters by requiring compile-time format strings for routes (e.g. "/foo/%s/%d) or known types (e.g. FooUrlParameters).
With either approach, Giraffe will emit a 400 error response if parameter binding fails (e.g. if a string is provided where an int was expected).
Currently, I am not aware of any way to customize this response, meaning if your OpenAPI schema specifies a different response code for an incorrectly formatted URL parameter, this will basically be ignored.
To ensure your OpenAPI schema and implementation are consistent, I suggest ensuring that your schema only specifies return code 400 for incorrectly formatted URL parameters.
If you have any suggestions for customizing this, please file an issue.
## Service Implementations
Stubbed service implementations of those interfaces have been generated as follows:
- impl/PetApiService.fs
- impl/StoreApiService.fs
- impl/UserApiService.fs
You should manually edit these files to implement your business logic.
## Additional Handlers
Additional handlers can be configured in the Customization.fs
`let handlers : HttpHandler list = [
// insert your handlers here
GET >=>
choose [
route "/login" >=> redirectToLogin
route "/logout" >=> logout
]
]`
## Authentication
### OAuth
If your OpenAPI spec contains oAuth2 securitySchemes, these will have been auto-generated.
To configure any of these, you must set the "xxxClientId" and "xxxClientSecret" environment variables (e.g. "GoogleClientId", "GoogleClientSecret") where xxx is the securityScheme ID.
If you specify the securityScheme ID as "Google" or "GitHub" (note the capital "G" and "H" in the latter), the generator will default to:
- for Google, the [ASP.NET Core providers](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?view=aspnetcore-2.2)
- for GitHub, the [aspnet-contrib provider](https://www.nuget.org/packages/AspNet.Security.OAuth.GitHub/)
For any other ID (e.g. "Facebook"), a [generic ASP.NET Core oAuth provider](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.oauthextensions.addoauth?view=aspnetcore-2.2) will be configured.
See impl/AuthSchemes.fs for further details.
NOTE - currently, authentication against ANY defined OAuth scheme will allow access to a handler (even if the scheme was not specified as a security scheme for the particular handler).
This is on the TODO list.
### API key
API key authentication is supported via the (AspNet.Security.ApiKey.Providers package)[https://github.com/jamesharling/AspNet.Security.ApiKey.Providers].
You must implement your own validation logic for the key in CustomHandlers.setApiKeyEvents.
## TODO/currently unsupported
- form request bodies (URL-encoded or multipart)
- implicit oAuth
- limit handler access to specified oAuth scheme when multiple oAuth schemes defined
- XML content/response types
- http authentication
- testing header params
## .openapi-generator-ignore
It is recommended to add src/impl/** and the project's .fsproj file to the .openapi-generator-ignore file.
This will allow you to regenerate model, operation and parameter files without overriding your implementations of business logic, authentication, data layers, and so on.
## Build and test the application
### Windows
Run the `build.bat` script in order to restore, build and test (if you've selected to include tests) the application:
```
> ./build.bat
```
### Linux/macOS
Run the `build.sh` script in order to restore, build and test (if you've selected to include tests) the application:
```
$ ./build.sh
```
## Run the application
After a successful build you can start the web application by executing the following command in your terminal:
```
dotnet run --project src/{{packageName}
```
After the application has started visit [http://localhost:5000](http://localhost:5000) in your preferred browser.

View File

@ -0,0 +1,3 @@
dotnet restore src/OpenAPI.fsproj
dotnet build src/OpenAPI.fsproj

View File

@ -0,0 +1,4 @@
#!/bin/sh
dotnet restore src/OpenAPI.fsproj
dotnet build src/OpenAPI.fsproj

View File

@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<Description>OpenAPI</Description>
<Copyright>OpenAPI</Copyright>
<TargetFramework>netcoreapp2.2</TargetFramework>
<DebugType>portable</DebugType>
<EnableDefaultContentItems>false</EnableDefaultContentItems>
<RunWorkingDirectory>$(MSBuildThisFileDirectory)</RunWorkingDirectory>
<AssemblyName>OpenAPI</AssemblyName>
<PackageId>OpenAPI</PackageId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" />
<PackageReference Include="Giraffe" Version="3.4.*" />
<PackageReference Include="TaskBuilder.fs" Version="2.1.*" />
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="2.0.1" />
<PackageReference Include="AspNet.Security.ApiKey.Providers" Version="1.1.0" />
</ItemGroup>
<ItemGroup>
<Content Include="openapi.yaml"/>
<Compile Include="helpers/Helpers.fs" />
<Compile Include="model/ApiResponse.fs" />
<Compile Include="model/User.fs" />
<Compile Include="model/Tag.fs" />
<Compile Include="model/Category.fs" />
<Compile Include="model/Order.fs" />
<Compile Include="model/InlineObject1.fs" />
<Compile Include="model/InlineObject.fs" />
<Compile Include="model/Pet.fs" />
<Compile Include="api/PetApiHandlerParams.fs" />
<Compile Include="api/PetApiServiceInterface.fs" />
<Compile Include="impl/PetApiService.fs" />
<Compile Include="api/PetApiHandler.fs" />
<Compile Include="api/StoreApiHandlerParams.fs" />
<Compile Include="api/StoreApiServiceInterface.fs" />
<Compile Include="impl/StoreApiService.fs" />
<Compile Include="api/StoreApiHandler.fs" />
<Compile Include="api/UserApiHandlerParams.fs" />
<Compile Include="api/UserApiServiceInterface.fs" />
<Compile Include="impl/UserApiService.fs" />
<Compile Include="api/UserApiHandler.fs" />
<Compile Include="impl/CustomHandlers.fs" />
<Compile Include="auth/AuthSchemes.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,109 @@
namespace OpenAPI
open System
open System.Net.Http
open System.Security.Claims
open System.Threading
open Microsoft.AspNetCore
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Http.Features
open Microsoft.AspNetCore.Authentication
open Microsoft.AspNetCore.Authentication.Cookies
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.Logging
open Microsoft.Extensions.DependencyInjection
open FSharp.Control.Tasks.V2.ContextInsensitive
open System.Diagnostics
open Giraffe.GiraffeViewEngine
open AspNet.Security.ApiKey.Providers
open PetApiHandlerParams
open StoreApiHandlerParams
open UserApiHandlerParams
open Giraffe
module App =
// ---------------------------------
// Error handler
// ---------------------------------
let errorHandler (ex : Exception) (logger : ILogger) =
logger.LogError(EventId(), ex, "An unhandled exception has occurred while executing the request.")
clearResponse >=> setStatusCode 500 >=> text ex.Message
// ---------------------------------
// Web app
// ---------------------------------
let HttpGet = GET
let HttpPost = POST
let HttpPut = PUT
let HttpDelete = DELETE
let authFailure : HttpHandler =
setStatusCode 401 >=> text "You must be authenticated to access this resource."
let webApp =
choose (CustomHandlers.handlers @ [
HttpPost >=> route "/v2/pet" >=> requiresAuthentication authFailure >=> PetApiHandler.AddPet;
HttpDelete >=> routeBind<DeletePetPathParams> "/v2/pet/{petId}" (fun x -> requiresAuthentication authFailure >=> PetApiHandler.DeletePet x);
HttpGet >=> route "/v2/pet/findByStatus" >=> requiresAuthentication authFailure >=> PetApiHandler.FindPetsByStatus;
HttpGet >=> route "/v2/pet/findByTags" >=> requiresAuthentication authFailure >=> PetApiHandler.FindPetsByTags;
HttpGet >=> routeBind<GetPetByIdPathParams> "/v2/pet/{petId}" (fun x -> challenge ApiKeyDefaults.AuthenticationScheme >=> requiresAuthentication authFailure >=> PetApiHandler.GetPetById x);
HttpPut >=> route "/v2/pet" >=> requiresAuthentication authFailure >=> PetApiHandler.UpdatePet;
HttpPost >=> routeBind<UpdatePetWithFormPathParams> "/v2/pet/{petId}" (fun x -> requiresAuthentication authFailure >=> PetApiHandler.UpdatePetWithForm x);
HttpPost >=> routeBind<UploadFilePathParams> "/v2/pet/{petId}/uploadImage" (fun x -> requiresAuthentication authFailure >=> PetApiHandler.UploadFile x);
HttpDelete >=> routeBind<DeleteOrderPathParams> "/v2/store/order/{orderId}" (fun x -> StoreApiHandler.DeleteOrder x);
HttpGet >=> route "/v2/store/inventory" >=> challenge ApiKeyDefaults.AuthenticationScheme >=> requiresAuthentication authFailure >=> StoreApiHandler.GetInventory;
HttpGet >=> routeBind<GetOrderByIdPathParams> "/v2/store/order/{orderId}" (fun x -> StoreApiHandler.GetOrderById x);
HttpPost >=> route "/v2/store/order" >=> StoreApiHandler.PlaceOrder;
HttpPost >=> route "/v2/user" >=> challenge ApiKeyDefaults.AuthenticationScheme >=> requiresAuthentication authFailure >=> UserApiHandler.CreateUser;
HttpPost >=> route "/v2/user/createWithArray" >=> challenge ApiKeyDefaults.AuthenticationScheme >=> requiresAuthentication authFailure >=> UserApiHandler.CreateUsersWithArrayInput;
HttpPost >=> route "/v2/user/createWithList" >=> challenge ApiKeyDefaults.AuthenticationScheme >=> requiresAuthentication authFailure >=> UserApiHandler.CreateUsersWithListInput;
HttpDelete >=> routeBind<DeleteUserPathParams> "/v2/user/{username}" (fun x -> challenge ApiKeyDefaults.AuthenticationScheme >=> requiresAuthentication authFailure >=> UserApiHandler.DeleteUser x);
HttpGet >=> routeBind<GetUserByNamePathParams> "/v2/user/{username}" (fun x -> UserApiHandler.GetUserByName x);
HttpGet >=> route "/v2/user/login" >=> UserApiHandler.LoginUser;
HttpGet >=> route "/v2/user/logout" >=> challenge ApiKeyDefaults.AuthenticationScheme >=> requiresAuthentication authFailure >=> UserApiHandler.LogoutUser;
HttpPut >=> routeBind<UpdateUserPathParams> "/v2/user/{username}" (fun x -> challenge ApiKeyDefaults.AuthenticationScheme >=> requiresAuthentication authFailure >=> UserApiHandler.UpdateUser x);
RequestErrors.notFound (text "Not Found")
])
// ---------------------------------
// Main
// ---------------------------------
let configureApp (app : IApplicationBuilder) =
app.UseGiraffeErrorHandler(errorHandler)
.UseStaticFiles()
.UseAuthentication()
.UseResponseCaching() |> ignore
CustomHandlers.configureApp app |> ignore
app.UseGiraffe webApp |> ignore
let configureServices (services : IServiceCollection) =
services
.AddResponseCaching()
.AddGiraffe()
|> AuthSchemes.configureServices
|> CustomHandlers.configureServices services
|> ignore
services.AddDataProtection() |> ignore
let configureLogging (loggerBuilder : ILoggingBuilder) =
loggerBuilder.AddFilter(fun lvl -> lvl.Equals LogLevel.Error)
.AddConsole()
.AddDebug() |> ignore
[<EntryPoint>]
let main _ =
let builder = WebHost.CreateDefaultBuilder()
.Configure(Action<IApplicationBuilder> configureApp)
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
|> CustomHandlers.configureWebHost
builder.Build()
.Run()
0

View File

@ -0,0 +1,179 @@
namespace OpenAPI
open System.Collections.Generic
open Giraffe
open Microsoft.AspNetCore.Http
open FSharp.Control.Tasks.V2.ContextInsensitive
open PetApiHandlerParams
open PetApiServiceInterface
open PetApiServiceImplementation
open OpenAPI.Model.ApiResponse
open OpenAPI.Model.Pet
module PetApiHandler =
/// <summary>
///
/// </summary>
//#region AddPet
/// <summary>
/// Add a new pet to the store
/// </summary>
let AddPet : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! bodyParams =
ctx.BindJsonAsync<AddPetBodyParams>()
let serviceArgs = { bodyParams=bodyParams } : AddPetArgs
let result = PetApiService.AddPet ctx serviceArgs
return! (match result with
| AddPetStatusCode405 resolved ->
setStatusCode 405 >=> text resolved.content
) next ctx
}
//#endregion
//#region DeletePet
/// <summary>
/// Deletes a pet
/// </summary>
let DeletePet (pathParams:DeletePetPathParams) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let headerParams = {
DeletePetHeaderParams.apiKey=ctx.TryGetRequestHeader "apiKey";
}
let serviceArgs = { headerParams=headerParams; pathParams=pathParams; } : DeletePetArgs
let result = PetApiService.DeletePet ctx serviceArgs
return! (match result with
| DeletePetStatusCode400 resolved ->
setStatusCode 400 >=> text resolved.content
) next ctx
}
//#endregion
//#region FindPetsByStatus
/// <summary>
/// Finds Pets by status
/// </summary>
let FindPetsByStatus : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let queryParams = ctx.TryBindQueryString<FindPetsByStatusQueryParams>()
let serviceArgs = { queryParams=queryParams; } : FindPetsByStatusArgs
let result = PetApiService.FindPetsByStatus ctx serviceArgs
return! (match result with
| FindPetsByStatusDefaultStatusCode resolved ->
setStatusCode 200 >=> json resolved.content
| FindPetsByStatusStatusCode400 resolved ->
setStatusCode 400 >=> text resolved.content
) next ctx
}
//#endregion
//#region FindPetsByTags
/// <summary>
/// Finds Pets by tags
/// </summary>
let FindPetsByTags : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let queryParams = ctx.TryBindQueryString<FindPetsByTagsQueryParams>()
let serviceArgs = { queryParams=queryParams; } : FindPetsByTagsArgs
let result = PetApiService.FindPetsByTags ctx serviceArgs
return! (match result with
| FindPetsByTagsDefaultStatusCode resolved ->
setStatusCode 200 >=> json resolved.content
| FindPetsByTagsStatusCode400 resolved ->
setStatusCode 400 >=> text resolved.content
) next ctx
}
//#endregion
//#region GetPetById
/// <summary>
/// Find pet by ID
/// </summary>
let GetPetById (pathParams:GetPetByIdPathParams) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let serviceArgs = { pathParams=pathParams; } : GetPetByIdArgs
let result = PetApiService.GetPetById ctx serviceArgs
return! (match result with
| GetPetByIdDefaultStatusCode resolved ->
setStatusCode 200 >=> json resolved.content
| GetPetByIdStatusCode400 resolved ->
setStatusCode 400 >=> text resolved.content
| GetPetByIdStatusCode404 resolved ->
setStatusCode 404 >=> text resolved.content
) next ctx
}
//#endregion
//#region UpdatePet
/// <summary>
/// Update an existing pet
/// </summary>
let UpdatePet : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! bodyParams =
ctx.BindJsonAsync<UpdatePetBodyParams>()
let serviceArgs = { bodyParams=bodyParams } : UpdatePetArgs
let result = PetApiService.UpdatePet ctx serviceArgs
return! (match result with
| UpdatePetStatusCode400 resolved ->
setStatusCode 400 >=> text resolved.content
| UpdatePetStatusCode404 resolved ->
setStatusCode 404 >=> text resolved.content
| UpdatePetStatusCode405 resolved ->
setStatusCode 405 >=> text resolved.content
) next ctx
}
//#endregion
//#region UpdatePetWithForm
/// <summary>
/// Updates a pet in the store with form data
/// </summary>
let UpdatePetWithForm (pathParams:UpdatePetWithFormPathParams) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! formParams = ctx.TryBindFormAsync<UpdatePetWithFormFormParams>()
let serviceArgs = { formParams=formParams; pathParams=pathParams; } : UpdatePetWithFormArgs
let result = PetApiService.UpdatePetWithForm ctx serviceArgs
return! (match result with
| UpdatePetWithFormStatusCode405 resolved ->
setStatusCode 405 >=> text resolved.content
) next ctx
}
//#endregion
//#region UploadFile
/// <summary>
/// uploads an image
/// </summary>
let UploadFile (pathParams:UploadFilePathParams) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! formParams = ctx.TryBindFormAsync<UploadFileFormParams>()
let serviceArgs = { formParams=formParams; pathParams=pathParams; } : UploadFileArgs
let result = PetApiService.UploadFile ctx serviceArgs
return! (match result with
| UploadFileDefaultStatusCode resolved ->
setStatusCode 200 >=> json resolved.content
) next ctx
}
//#endregion

View File

@ -0,0 +1,213 @@
namespace OpenAPI
open OpenAPI.Model.ApiResponse
open OpenAPI.Model.Pet
open System.Collections.Generic
open System
module PetApiHandlerParams =
//#region Body parameters
[<CLIMutable>]
type AddPetBodyParams = Pet
//#endregion
type AddPetStatusCode405Response = {
content:string;
}
type AddPetResult = AddPetStatusCode405 of AddPetStatusCode405Response
type AddPetArgs = {
bodyParams:AddPetBodyParams
}
//#region Path parameters
[<CLIMutable>]
type DeletePetPathParams = {
petId : int64 ;
}
//#endregion
//#region Header parameters
[<CLIMutable>]
type DeletePetHeaderParams = {
apiKey : string option;
}
//#endregion
type DeletePetStatusCode400Response = {
content:string;
}
type DeletePetResult = DeletePetStatusCode400 of DeletePetStatusCode400Response
type DeletePetArgs = {
headerParams:DeletePetHeaderParams;
pathParams:DeletePetPathParams;
}
//#region Query parameters
[<CLIMutable>]
type FindPetsByStatusQueryParams = {
status : string[] ;
}
//#endregion
type FindPetsByStatusDefaultStatusCodeResponse = {
content:Pet[];
}
type FindPetsByStatusStatusCode400Response = {
content:string;
}
type FindPetsByStatusResult = FindPetsByStatusDefaultStatusCode of FindPetsByStatusDefaultStatusCodeResponse|FindPetsByStatusStatusCode400 of FindPetsByStatusStatusCode400Response
type FindPetsByStatusArgs = {
queryParams:Result<FindPetsByStatusQueryParams,string>;
}
//#region Query parameters
[<CLIMutable>]
type FindPetsByTagsQueryParams = {
tags : string[] ;
maxCount : int option;
}
//#endregion
type FindPetsByTagsDefaultStatusCodeResponse = {
content:Pet[];
}
type FindPetsByTagsStatusCode400Response = {
content:string;
}
type FindPetsByTagsResult = FindPetsByTagsDefaultStatusCode of FindPetsByTagsDefaultStatusCodeResponse|FindPetsByTagsStatusCode400 of FindPetsByTagsStatusCode400Response
type FindPetsByTagsArgs = {
queryParams:Result<FindPetsByTagsQueryParams,string>;
}
//#region Path parameters
[<CLIMutable>]
type GetPetByIdPathParams = {
petId : int64 ;
}
//#endregion
type GetPetByIdDefaultStatusCodeResponse = {
content:Pet;
}
type GetPetByIdStatusCode400Response = {
content:string;
}
type GetPetByIdStatusCode404Response = {
content:string;
}
type GetPetByIdResult = GetPetByIdDefaultStatusCode of GetPetByIdDefaultStatusCodeResponse|GetPetByIdStatusCode400 of GetPetByIdStatusCode400Response|GetPetByIdStatusCode404 of GetPetByIdStatusCode404Response
type GetPetByIdArgs = {
pathParams:GetPetByIdPathParams;
}
//#region Body parameters
[<CLIMutable>]
type UpdatePetBodyParams = Pet
//#endregion
type UpdatePetStatusCode400Response = {
content:string;
}
type UpdatePetStatusCode404Response = {
content:string;
}
type UpdatePetStatusCode405Response = {
content:string;
}
type UpdatePetResult = UpdatePetStatusCode400 of UpdatePetStatusCode400Response|UpdatePetStatusCode404 of UpdatePetStatusCode404Response|UpdatePetStatusCode405 of UpdatePetStatusCode405Response
type UpdatePetArgs = {
bodyParams:UpdatePetBodyParams
}
//#region Path parameters
[<CLIMutable>]
type UpdatePetWithFormPathParams = {
petId : int64 ;
}
//#endregion
//#region Form parameters
[<CLIMutable>]
type UpdatePetWithFormFormParams = {
name : string option;
//#endregion
//#region Form parameters
status : string option;
}
//#endregion
type UpdatePetWithFormStatusCode405Response = {
content:string;
}
type UpdatePetWithFormResult = UpdatePetWithFormStatusCode405 of UpdatePetWithFormStatusCode405Response
type UpdatePetWithFormArgs = {
pathParams:UpdatePetWithFormPathParams;
formParams:Result<UpdatePetWithFormFormParams,string>
}
//#region Path parameters
[<CLIMutable>]
type UploadFilePathParams = {
petId : int64 ;
}
//#endregion
//#region Form parameters
[<CLIMutable>]
type UploadFileFormParams = {
additionalMetadata : string option;
//#endregion
//#region Form parameters
file : System.IO.Stream option;
}
//#endregion
type UploadFileDefaultStatusCodeResponse = {
content:ApiResponse;
}
type UploadFileResult = UploadFileDefaultStatusCode of UploadFileDefaultStatusCodeResponse
type UploadFileArgs = {
pathParams:UploadFilePathParams;
formParams:Result<UploadFileFormParams,string>
}

View File

@ -0,0 +1,20 @@
namespace OpenAPI
open PetApiHandlerParams
open System
open Giraffe
open Microsoft.AspNetCore.Http
module PetApiServiceInterface =
//#region Service interface
type IPetApiService =
abstract member AddPet:HttpContext -> AddPetArgs->AddPetResult
abstract member DeletePet:HttpContext -> DeletePetArgs->DeletePetResult
abstract member FindPetsByStatus:HttpContext -> FindPetsByStatusArgs->FindPetsByStatusResult
abstract member FindPetsByTags:HttpContext -> FindPetsByTagsArgs->FindPetsByTagsResult
abstract member GetPetById:HttpContext -> GetPetByIdArgs->GetPetByIdResult
abstract member UpdatePet:HttpContext -> UpdatePetArgs->UpdatePetResult
abstract member UpdatePetWithForm:HttpContext -> UpdatePetWithFormArgs->UpdatePetWithFormResult
abstract member UploadFile:HttpContext -> UploadFileArgs->UploadFileResult
//#endregion

View File

@ -0,0 +1,97 @@
namespace OpenAPI
open System.Collections.Generic
open Giraffe
open Microsoft.AspNetCore.Http
open FSharp.Control.Tasks.V2.ContextInsensitive
open StoreApiHandlerParams
open StoreApiServiceInterface
open StoreApiServiceImplementation
open System.Collections.Generic
open OpenAPI.Model.Order
module StoreApiHandler =
/// <summary>
///
/// </summary>
//#region DeleteOrder
/// <summary>
/// Delete purchase order by ID
/// </summary>
let DeleteOrder (pathParams:DeleteOrderPathParams) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let serviceArgs = { pathParams=pathParams; } : DeleteOrderArgs
let result = StoreApiService.DeleteOrder ctx serviceArgs
return! (match result with
| DeleteOrderStatusCode400 resolved ->
setStatusCode 400 >=> text resolved.content
| DeleteOrderStatusCode404 resolved ->
setStatusCode 404 >=> text resolved.content
) next ctx
}
//#endregion
//#region GetInventory
/// <summary>
/// Returns pet inventories by status
/// </summary>
let GetInventory : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let result = StoreApiService.GetInventory ctx
return! (match result with
| GetInventoryDefaultStatusCode resolved ->
setStatusCode 200 >=> json resolved.content
) next ctx
}
//#endregion
//#region GetOrderById
/// <summary>
/// Find purchase order by ID
/// </summary>
let GetOrderById (pathParams:GetOrderByIdPathParams) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let serviceArgs = { pathParams=pathParams; } : GetOrderByIdArgs
let result = StoreApiService.GetOrderById ctx serviceArgs
return! (match result with
| GetOrderByIdDefaultStatusCode resolved ->
setStatusCode 200 >=> json resolved.content
| GetOrderByIdStatusCode400 resolved ->
setStatusCode 400 >=> text resolved.content
| GetOrderByIdStatusCode404 resolved ->
setStatusCode 404 >=> text resolved.content
) next ctx
}
//#endregion
//#region PlaceOrder
/// <summary>
/// Place an order for a pet
/// </summary>
let PlaceOrder : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! bodyParams =
ctx.BindJsonAsync<PlaceOrderBodyParams>()
let serviceArgs = { bodyParams=bodyParams } : PlaceOrderArgs
let result = StoreApiService.PlaceOrder ctx serviceArgs
return! (match result with
| PlaceOrderDefaultStatusCode resolved ->
setStatusCode 200 >=> json resolved.content
| PlaceOrderStatusCode400 resolved ->
setStatusCode 400 >=> text resolved.content
) next ctx
}
//#endregion

View File

@ -0,0 +1,88 @@
namespace OpenAPI
open System.Collections.Generic
open OpenAPI.Model.Order
open System.Collections.Generic
open System
module StoreApiHandlerParams =
//#region Path parameters
[<CLIMutable>]
type DeleteOrderPathParams = {
orderId : string ;
}
//#endregion
type DeleteOrderStatusCode400Response = {
content:string;
}
type DeleteOrderStatusCode404Response = {
content:string;
}
type DeleteOrderResult = DeleteOrderStatusCode400 of DeleteOrderStatusCode400Response|DeleteOrderStatusCode404 of DeleteOrderStatusCode404Response
type DeleteOrderArgs = {
pathParams:DeleteOrderPathParams;
}
type GetInventoryDefaultStatusCodeResponse = {
content:IDictionary<string, int>;
}
type GetInventoryResult = GetInventoryDefaultStatusCode of GetInventoryDefaultStatusCodeResponse
//#region Path parameters
[<CLIMutable>]
type GetOrderByIdPathParams = {
orderId : int64 ;
}
//#endregion
type GetOrderByIdDefaultStatusCodeResponse = {
content:Order;
}
type GetOrderByIdStatusCode400Response = {
content:string;
}
type GetOrderByIdStatusCode404Response = {
content:string;
}
type GetOrderByIdResult = GetOrderByIdDefaultStatusCode of GetOrderByIdDefaultStatusCodeResponse|GetOrderByIdStatusCode400 of GetOrderByIdStatusCode400Response|GetOrderByIdStatusCode404 of GetOrderByIdStatusCode404Response
type GetOrderByIdArgs = {
pathParams:GetOrderByIdPathParams;
}
//#region Body parameters
[<CLIMutable>]
type PlaceOrderBodyParams = Order
//#endregion
type PlaceOrderDefaultStatusCodeResponse = {
content:Order;
}
type PlaceOrderStatusCode400Response = {
content:string;
}
type PlaceOrderResult = PlaceOrderDefaultStatusCode of PlaceOrderDefaultStatusCodeResponse|PlaceOrderStatusCode400 of PlaceOrderStatusCode400Response
type PlaceOrderArgs = {
bodyParams:PlaceOrderBodyParams
}

View File

@ -0,0 +1,16 @@
namespace OpenAPI
open StoreApiHandlerParams
open System
open Giraffe
open Microsoft.AspNetCore.Http
module StoreApiServiceInterface =
//#region Service interface
type IStoreApiService =
abstract member DeleteOrder:HttpContext -> DeleteOrderArgs->DeleteOrderResult
abstract member GetInventory:HttpContext ->GetInventoryResult
abstract member GetOrderById:HttpContext -> GetOrderByIdArgs->GetOrderByIdResult
abstract member PlaceOrder:HttpContext -> PlaceOrderArgs->PlaceOrderResult
//#endregion

View File

@ -0,0 +1,173 @@
namespace OpenAPI
open System.Collections.Generic
open Giraffe
open Microsoft.AspNetCore.Http
open FSharp.Control.Tasks.V2.ContextInsensitive
open UserApiHandlerParams
open UserApiServiceInterface
open UserApiServiceImplementation
open OpenAPI.Model.User
module UserApiHandler =
/// <summary>
///
/// </summary>
//#region CreateUser
/// <summary>
/// Create user
/// </summary>
let CreateUser : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! bodyParams =
ctx.BindJsonAsync<CreateUserBodyParams>()
let serviceArgs = { bodyParams=bodyParams } : CreateUserArgs
let result = UserApiService.CreateUser ctx serviceArgs
return! (match result with
| CreateUserDefaultStatusCode resolved ->
setStatusCode 0 >=> text resolved.content
) next ctx
}
//#endregion
//#region CreateUsersWithArrayInput
/// <summary>
/// Creates list of users with given input array
/// </summary>
let CreateUsersWithArrayInput : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! bodyParams =
ctx.BindJsonAsync<CreateUsersWithArrayInputBodyParams>()
let serviceArgs = { bodyParams=bodyParams } : CreateUsersWithArrayInputArgs
let result = UserApiService.CreateUsersWithArrayInput ctx serviceArgs
return! (match result with
| CreateUsersWithArrayInputDefaultStatusCode resolved ->
setStatusCode 0 >=> text resolved.content
) next ctx
}
//#endregion
//#region CreateUsersWithListInput
/// <summary>
/// Creates list of users with given input array
/// </summary>
let CreateUsersWithListInput : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! bodyParams =
ctx.BindJsonAsync<CreateUsersWithListInputBodyParams>()
let serviceArgs = { bodyParams=bodyParams } : CreateUsersWithListInputArgs
let result = UserApiService.CreateUsersWithListInput ctx serviceArgs
return! (match result with
| CreateUsersWithListInputDefaultStatusCode resolved ->
setStatusCode 0 >=> text resolved.content
) next ctx
}
//#endregion
//#region DeleteUser
/// <summary>
/// Delete user
/// </summary>
let DeleteUser (pathParams:DeleteUserPathParams) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let serviceArgs = { pathParams=pathParams; } : DeleteUserArgs
let result = UserApiService.DeleteUser ctx serviceArgs
return! (match result with
| DeleteUserStatusCode400 resolved ->
setStatusCode 400 >=> text resolved.content
| DeleteUserStatusCode404 resolved ->
setStatusCode 404 >=> text resolved.content
) next ctx
}
//#endregion
//#region GetUserByName
/// <summary>
/// Get user by user name
/// </summary>
let GetUserByName (pathParams:GetUserByNamePathParams) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let serviceArgs = { pathParams=pathParams; } : GetUserByNameArgs
let result = UserApiService.GetUserByName ctx serviceArgs
return! (match result with
| GetUserByNameDefaultStatusCode resolved ->
setStatusCode 200 >=> json resolved.content
| GetUserByNameStatusCode400 resolved ->
setStatusCode 400 >=> text resolved.content
| GetUserByNameStatusCode404 resolved ->
setStatusCode 404 >=> text resolved.content
) next ctx
}
//#endregion
//#region LoginUser
/// <summary>
/// Logs user into the system
/// </summary>
let LoginUser : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let queryParams = ctx.TryBindQueryString<LoginUserQueryParams>()
let serviceArgs = { queryParams=queryParams; } : LoginUserArgs
let result = UserApiService.LoginUser ctx serviceArgs
return! (match result with
| LoginUserDefaultStatusCode resolved ->
setStatusCode 200 >=> text resolved.content
| LoginUserStatusCode400 resolved ->
setStatusCode 400 >=> text resolved.content
) next ctx
}
//#endregion
//#region LogoutUser
/// <summary>
/// Logs out current logged in user session
/// </summary>
let LogoutUser : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let result = UserApiService.LogoutUser ctx
return! (match result with
| LogoutUserDefaultStatusCode resolved ->
setStatusCode 0 >=> text resolved.content
) next ctx
}
//#endregion
//#region UpdateUser
/// <summary>
/// Updated user
/// </summary>
let UpdateUser (pathParams:UpdateUserPathParams) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! bodyParams =
ctx.BindJsonAsync<UpdateUserBodyParams>()
let serviceArgs = { pathParams=pathParams; bodyParams=bodyParams } : UpdateUserArgs
let result = UserApiService.UpdateUser ctx serviceArgs
return! (match result with
| UpdateUserStatusCode400 resolved ->
setStatusCode 400 >=> text resolved.content
| UpdateUserStatusCode404 resolved ->
setStatusCode 404 >=> text resolved.content
) next ctx
}
//#endregion

View File

@ -0,0 +1,169 @@
namespace OpenAPI
open OpenAPI.Model.User
open System.Collections.Generic
open System
module UserApiHandlerParams =
//#region Body parameters
[<CLIMutable>]
type CreateUserBodyParams = User
//#endregion
type CreateUserDefaultStatusCodeResponse = {
content:string;
}
type CreateUserResult = CreateUserDefaultStatusCode of CreateUserDefaultStatusCodeResponse
type CreateUserArgs = {
bodyParams:CreateUserBodyParams
}
//#region Body parameters
[<CLIMutable>]
type CreateUsersWithArrayInputBodyParams = User[]
//#endregion
type CreateUsersWithArrayInputDefaultStatusCodeResponse = {
content:string;
}
type CreateUsersWithArrayInputResult = CreateUsersWithArrayInputDefaultStatusCode of CreateUsersWithArrayInputDefaultStatusCodeResponse
type CreateUsersWithArrayInputArgs = {
bodyParams:CreateUsersWithArrayInputBodyParams
}
//#region Body parameters
[<CLIMutable>]
type CreateUsersWithListInputBodyParams = User[]
//#endregion
type CreateUsersWithListInputDefaultStatusCodeResponse = {
content:string;
}
type CreateUsersWithListInputResult = CreateUsersWithListInputDefaultStatusCode of CreateUsersWithListInputDefaultStatusCodeResponse
type CreateUsersWithListInputArgs = {
bodyParams:CreateUsersWithListInputBodyParams
}
//#region Path parameters
[<CLIMutable>]
type DeleteUserPathParams = {
username : string ;
}
//#endregion
type DeleteUserStatusCode400Response = {
content:string;
}
type DeleteUserStatusCode404Response = {
content:string;
}
type DeleteUserResult = DeleteUserStatusCode400 of DeleteUserStatusCode400Response|DeleteUserStatusCode404 of DeleteUserStatusCode404Response
type DeleteUserArgs = {
pathParams:DeleteUserPathParams;
}
//#region Path parameters
[<CLIMutable>]
type GetUserByNamePathParams = {
username : string ;
}
//#endregion
type GetUserByNameDefaultStatusCodeResponse = {
content:User;
}
type GetUserByNameStatusCode400Response = {
content:string;
}
type GetUserByNameStatusCode404Response = {
content:string;
}
type GetUserByNameResult = GetUserByNameDefaultStatusCode of GetUserByNameDefaultStatusCodeResponse|GetUserByNameStatusCode400 of GetUserByNameStatusCode400Response|GetUserByNameStatusCode404 of GetUserByNameStatusCode404Response
type GetUserByNameArgs = {
pathParams:GetUserByNamePathParams;
}
//#region Query parameters
[<CLIMutable>]
type LoginUserQueryParams = {
username : string ;
password : string ;
}
//#endregion
type LoginUserDefaultStatusCodeResponse = {
content:string;
}
type LoginUserStatusCode400Response = {
content:string;
}
type LoginUserResult = LoginUserDefaultStatusCode of LoginUserDefaultStatusCodeResponse|LoginUserStatusCode400 of LoginUserStatusCode400Response
type LoginUserArgs = {
queryParams:Result<LoginUserQueryParams,string>;
}
type LogoutUserDefaultStatusCodeResponse = {
content:string;
}
type LogoutUserResult = LogoutUserDefaultStatusCode of LogoutUserDefaultStatusCodeResponse
//#region Path parameters
[<CLIMutable>]
type UpdateUserPathParams = {
username : string ;
}
//#endregion
//#region Body parameters
[<CLIMutable>]
type UpdateUserBodyParams = User
//#endregion
type UpdateUserStatusCode400Response = {
content:string;
}
type UpdateUserStatusCode404Response = {
content:string;
}
type UpdateUserResult = UpdateUserStatusCode400 of UpdateUserStatusCode400Response|UpdateUserStatusCode404 of UpdateUserStatusCode404Response
type UpdateUserArgs = {
pathParams:UpdateUserPathParams;
bodyParams:UpdateUserBodyParams
}

View File

@ -0,0 +1,20 @@
namespace OpenAPI
open UserApiHandlerParams
open System
open Giraffe
open Microsoft.AspNetCore.Http
module UserApiServiceInterface =
//#region Service interface
type IUserApiService =
abstract member CreateUser:HttpContext -> CreateUserArgs->CreateUserResult
abstract member CreateUsersWithArrayInput:HttpContext -> CreateUsersWithArrayInputArgs->CreateUsersWithArrayInputResult
abstract member CreateUsersWithListInput:HttpContext -> CreateUsersWithListInputArgs->CreateUsersWithListInputResult
abstract member DeleteUser:HttpContext -> DeleteUserArgs->DeleteUserResult
abstract member GetUserByName:HttpContext -> GetUserByNameArgs->GetUserByNameResult
abstract member LoginUser:HttpContext -> LoginUserArgs->LoginUserResult
abstract member LogoutUser:HttpContext ->LogoutUserResult
abstract member UpdateUser:HttpContext -> UpdateUserArgs->UpdateUserResult
//#endregion

View File

@ -0,0 +1,88 @@
namespace OpenAPI
open Microsoft.AspNetCore.Authentication
open Microsoft.AspNetCore.Authentication.Cookies
open Microsoft.Extensions.DependencyInjection
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Authentication.OAuth
open System
open Giraffe
open FSharp.Control.Tasks.V2.ContextInsensitive
open Microsoft.Extensions.Configuration
open AspNet.Security.ApiKey.Providers.Extensions
open AspNet.Security.ApiKey.Providers.Events
module AuthSchemes =
let accessDenied : HttpHandler = setStatusCode 401 >=> text "Access Denied"
let buildGoogle (builder:AuthenticationBuilder) name authorizationUrl scopes (settings:IConfiguration) =
builder.AddGoogle(fun googleOptions -> CustomHandlers.setOAuthOptions "Google" googleOptions scopes settings)
let buildGitHub (builder:AuthenticationBuilder) name authorizationUrl scopes (settings:IConfiguration) =
builder.AddGitHub(fun githubOptions -> CustomHandlers.setOAuthOptions "GitHub" githubOptions scopes settings)
let buildOAuth (builder:AuthenticationBuilder) (name:string) authorizationUrl scopes (settings:IConfiguration) =
builder.AddOAuth(name, (fun (options:OAuthOptions) ->
options.AuthorizationEndpoint <- authorizationUrl
options.TokenEndpoint <- settings.[name + "TokenUrl"]
options.CallbackPath <- PathString(settings.[name + "CallbackPath"])
CustomHandlers.setOAuthOptions "" options scopes settings
))
let OAuthBuilders = Map.empty.Add("Google", buildGoogle).Add("GitHub", buildGitHub)
let checkEnvironment (settings:IConfiguration) name =
if (isNull settings.[name + "ClientId"]) then
raise (Exception(name + "ClientId is not set."))
else if (isNull settings.[name + "ClientSecret"]) then
raise (Exception((name + "ClientSecret is not set.")))
let getOAuthBuilder settings name =
// check that "xxxClientId" and "xxxClientSecret" configuration variables have been set for all OAuth providers
checkEnvironment settings name
if OAuthBuilders.ContainsKey(name) then
OAuthBuilders.[name]
else
buildOAuth
let configureOAuth (settings:IConfiguration) services =
(getOAuthBuilder settings "petstore_auth") services "petstore_auth" "http://petstore.swagger.io/api/oauth/dialog" ["write:pets";"read:pets";] settings
let buildApiKeyAuth name (services:AuthenticationBuilder) =
services.AddApiKey(fun options ->
options.Header <- name
options.HeaderKey <- String.Empty
let events = ApiKeyEvents()
options.Events <- CustomHandlers.setApiKeyEvents name events
)
let configureApiKeyAuth (settings:IConfiguration) services =
buildApiKeyAuth "api_key" services
raise (NotImplementedException("API key security scheme outside of header has not yet been implemented"))
let configureCookie (builder:AuthenticationBuilder) =
builder.AddCookie(CustomHandlers.cookieAuth)
let configureServices (services:IServiceCollection) =
let serviceProvider = services.BuildServiceProvider()
let settings = serviceProvider.GetService<IConfiguration>()
services.AddAuthentication(fun o -> o.DefaultScheme <- CookieAuthenticationDefaults.AuthenticationScheme)
|> configureOAuth settings
|> configureApiKeyAuth settings
|> configureCookie
let (|||) v1 v2 =
match v1 with
| Some v -> v1
| None -> v2
// this can be replaced with ctx.GetCookieValue in Giraffe >= 3.6
let getCookieValue (ctx:HttpContext) (key : string) =
match ctx.Request.Cookies.TryGetValue key with
| true , cookie -> Some cookie
| false, _ -> None

View File

@ -0,0 +1,12 @@
namespace OpenAPI
module Helpers =
let (>=>) switch1 switch2 =
match switch1 with
| Ok v1 ->
match switch2 with
| Ok v2 ->
Ok(v1, v2)
| Error e -> Error e
| Error e -> Error e

View File

@ -0,0 +1,112 @@
namespace OpenAPI
open System
open System.Net.Http
open System.Security.Claims
open System.Threading
open Microsoft.AspNetCore
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Http.Features
open Microsoft.AspNetCore.Authentication
open Microsoft.AspNetCore.Authentication.Cookies
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.Logging
open Microsoft.Extensions.DependencyInjection
open FSharp.Control.Tasks.V2.ContextInsensitive
open Giraffe
open Giraffe.GiraffeViewEngine
open Microsoft.AspNetCore.Authentication.OAuth
open System.Threading.Tasks
open AspNet.Security.ApiKey.Providers.Events
module CustomHandlers =
let cookieAuth (o : CookieAuthenticationOptions) =
do
o.Cookie.HttpOnly <- true
o.Cookie.SecurePolicy <- CookieSecurePolicy.SameAsRequest
o.SlidingExpiration <- true
o.ExpireTimeSpan <- TimeSpan.FromDays 7.0
let onCreatingTicket name (ctx:OAuthCreatingTicketContext) =
task {
// implement post-authentication logic for oAuth handlers here
()
} :> Task
let validateApiKey key =
raise (NotImplementedException("API key validation must be implemented"))
let setApiKeyEvents name (events:ApiKeyEvents) =
events.OnApiKeyValidated <- (fun ctx ->
task {
// implement your validation/authentication logic for api key handlers here
if validateApiKey ctx.ApiKey then
// to interact properly with Giraffe's handlers, you will need to manually set the identity
// let claims = ...
// let identity = ClaimsIdentity(claims, ApiKeyDefaults.AuthenticationScheme)
// ctx.HttpContext.User <- ClaimsPrincipal([|identity|])
ctx.Success()
} :> Task
)
events
let setOAuthOptions name (options:OAuthOptions) scopes (settings:IConfiguration) =
options.ClientId <- settings.[name + "ClientId"]
options.ClientSecret <- settings.[name + "ClientSecret"]
for scope in scopes do
options.Scope.Add scope
options.Events.OnCreatingTicket <- Func<OAuthCreatingTicketContext,Tasks.Task>(onCreatingTicket name)
match name with
| "Google" ->
()
| "GitHub" ->
()
| _ ->
()
let logout = signOut CookieAuthenticationDefaults.AuthenticationScheme >=> redirectTo false "/"
let loginView =
html [] [
head [] [
title [] [ str "Welcome" ]
]
body [] [
h1 [] [ str "Welcome" ]
a [_href "/login-with-api_key"] [ str "Login with api_key" ]
a [_href "/login-with-auth_cookie"] [ str "Login with auth_cookie" ]
a [_href "/login-with-petstore_auth"] [ str "Login with petstore_auth" ]
]
]
let redirectToLogin : HttpHandler =
htmlView loginView
let handlers : HttpHandler list = [
// insert your handlers here
GET >=>
choose [
route "/login" >=> redirectToLogin
route "/login-with-api_key" >=> challenge "api_key"
route "/login-with-auth_cookie" >=> challenge "auth_cookie"
route "/login-with-petstore_auth" >=> challenge "petstore_auth"
route "/logout" >=> logout
]
]
let configureWebHost (builder: IWebHostBuilder) =
// builder
// .UseContentRoot("content")
// .UseWebRoot("static")
builder
let configureApp (app : IApplicationBuilder) =
app
let configureServices (services:IServiceCollection) (authBuilder:AuthenticationBuilder) =
()

View File

@ -0,0 +1,72 @@
namespace OpenAPI
open OpenAPI.Model.ApiResponse
open OpenAPI.Model.Pet
open PetApiHandlerParams
open PetApiServiceInterface
open System.Collections.Generic
open System
open Giraffe
module PetApiServiceImplementation =
//#region Service implementation
type PetApiServiceImpl() =
interface IPetApiService with
member this.AddPet ctx args =
let content = "Invalid input"
AddPetStatusCode405 { content = content }
member this.DeletePet ctx args =
let content = "Invalid pet value"
DeletePetStatusCode400 { content = content }
member this.FindPetsByStatus ctx args =
if true then
let content = "successful operation" :> obj :?> Pet[] // this cast is obviously wrong, and is only intended to allow generated project to compile
FindPetsByStatusDefaultStatusCode { content = content }
else
let content = "Invalid status value"
FindPetsByStatusStatusCode400 { content = content }
member this.FindPetsByTags ctx args =
if true then
let content = "successful operation" :> obj :?> Pet[] // this cast is obviously wrong, and is only intended to allow generated project to compile
FindPetsByTagsDefaultStatusCode { content = content }
else
let content = "Invalid tag value"
FindPetsByTagsStatusCode400 { content = content }
member this.GetPetById ctx args =
if true then
let content = "successful operation" :> obj :?> Pet // this cast is obviously wrong, and is only intended to allow generated project to compile
GetPetByIdDefaultStatusCode { content = content }
else if true then
let content = "Invalid ID supplied"
GetPetByIdStatusCode400 { content = content }
else
let content = "Pet not found"
GetPetByIdStatusCode404 { content = content }
member this.UpdatePet ctx args =
if true then
let content = "Invalid ID supplied"
UpdatePetStatusCode400 { content = content }
else if true then
let content = "Pet not found"
UpdatePetStatusCode404 { content = content }
else
let content = "Validation exception"
UpdatePetStatusCode405 { content = content }
member this.UpdatePetWithForm ctx args =
let content = "Invalid input"
UpdatePetWithFormStatusCode405 { content = content }
member this.UploadFile ctx args =
let content = "successful operation" :> obj :?> ApiResponse // this cast is obviously wrong, and is only intended to allow generated project to compile
UploadFileDefaultStatusCode { content = content }
//#endregion
let PetApiService = PetApiServiceImpl() :> IPetApiService

View File

@ -0,0 +1,49 @@
namespace OpenAPI
open System.Collections.Generic
open OpenAPI.Model.Order
open StoreApiHandlerParams
open StoreApiServiceInterface
open System.Collections.Generic
open System
open Giraffe
module StoreApiServiceImplementation =
//#region Service implementation
type StoreApiServiceImpl() =
interface IStoreApiService with
member this.DeleteOrder ctx args =
if true then
let content = "Invalid ID supplied"
DeleteOrderStatusCode400 { content = content }
else
let content = "Order not found"
DeleteOrderStatusCode404 { content = content }
member this.GetInventory ctx =
let content = "successful operation" :> obj :?> IDictionary<string, int> // this cast is obviously wrong, and is only intended to allow generated project to compile
GetInventoryDefaultStatusCode { content = content }
member this.GetOrderById ctx args =
if true then
let content = "successful operation" :> obj :?> Order // this cast is obviously wrong, and is only intended to allow generated project to compile
GetOrderByIdDefaultStatusCode { content = content }
else if true then
let content = "Invalid ID supplied"
GetOrderByIdStatusCode400 { content = content }
else
let content = "Order not found"
GetOrderByIdStatusCode404 { content = content }
member this.PlaceOrder ctx args =
if true then
let content = "successful operation" :> obj :?> Order // this cast is obviously wrong, and is only intended to allow generated project to compile
PlaceOrderDefaultStatusCode { content = content }
else
let content = "Invalid Order"
PlaceOrderStatusCode400 { content = content }
//#endregion
let StoreApiService = StoreApiServiceImpl() :> IStoreApiService

View File

@ -0,0 +1,68 @@
namespace OpenAPI
open OpenAPI.Model.User
open UserApiHandlerParams
open UserApiServiceInterface
open System.Collections.Generic
open System
open Giraffe
module UserApiServiceImplementation =
//#region Service implementation
type UserApiServiceImpl() =
interface IUserApiService with
member this.CreateUser ctx args =
let content = "successful operation"
CreateUserDefaultStatusCode { content = content }
member this.CreateUsersWithArrayInput ctx args =
let content = "successful operation"
CreateUsersWithArrayInputDefaultStatusCode { content = content }
member this.CreateUsersWithListInput ctx args =
let content = "successful operation"
CreateUsersWithListInputDefaultStatusCode { content = content }
member this.DeleteUser ctx args =
if true then
let content = "Invalid username supplied"
DeleteUserStatusCode400 { content = content }
else
let content = "User not found"
DeleteUserStatusCode404 { content = content }
member this.GetUserByName ctx args =
if true then
let content = "successful operation" :> obj :?> User // this cast is obviously wrong, and is only intended to allow generated project to compile
GetUserByNameDefaultStatusCode { content = content }
else if true then
let content = "Invalid username supplied"
GetUserByNameStatusCode400 { content = content }
else
let content = "User not found"
GetUserByNameStatusCode404 { content = content }
member this.LoginUser ctx args =
if true then
let content = "successful operation" :> obj :?> string // this cast is obviously wrong, and is only intended to allow generated project to compile
LoginUserDefaultStatusCode { content = content }
else
let content = "Invalid username/password supplied"
LoginUserStatusCode400 { content = content }
member this.LogoutUser ctx =
let content = "successful operation"
LogoutUserDefaultStatusCode { content = content }
member this.UpdateUser ctx args =
if true then
let content = "Invalid user supplied"
UpdateUserStatusCode400 { content = content }
else
let content = "User not found"
UpdateUserStatusCode404 { content = content }
//#endregion
let UserApiService = UserApiServiceImpl() :> IUserApiService

View File

@ -0,0 +1,17 @@
namespace OpenAPI.Model
open System
open System.Collections.Generic
module ApiResponse =
//#region ApiResponse
type ApiResponse = {
Code : int;
Type : string;
Message : string;
}
//#endregion

View File

@ -0,0 +1,16 @@
namespace OpenAPI.Model
open System
open System.Collections.Generic
module Category =
//#region Category
type Category = {
Id : int64;
Name : string;
}
//#endregion

View File

@ -0,0 +1,16 @@
namespace OpenAPI.Model
open System
open System.Collections.Generic
module InlineObject =
//#region InlineObject
type inline_object = {
Name : string;
Status : string;
}
//#endregion

View File

@ -0,0 +1,16 @@
namespace OpenAPI.Model
open System
open System.Collections.Generic
module InlineObject1 =
//#region InlineObject1
type inline_object_1 = {
AdditionalMetadata : string;
File : System.IO.Stream;
}
//#endregion

View File

@ -0,0 +1,23 @@
namespace OpenAPI.Model
open System
open System.Collections.Generic
module Order =
//#region Order
//#region enums
type StatusEnum = PlacedEnum of string | ApprovedEnum of string | DeliveredEnum of string
//#endregion
type Order = {
Id : int64;
PetId : int64;
Quantity : int;
ShipDate : Nullable<DateTime>;
Status : StatusEnum;
Complete : bool;
}
//#endregion

View File

@ -0,0 +1,25 @@
namespace OpenAPI.Model
open System
open System.Collections.Generic
open OpenAPI.Model.Category
open OpenAPI.Model.Tag
module Pet =
//#region Pet
//#region enums
type StatusEnum = AvailableEnum of string | PendingEnum of string | SoldEnum of string
//#endregion
type Pet = {
Id : int64;
Category : Category;
Name : string;
PhotoUrls : string[];
Tags : Tag[];
Status : StatusEnum;
}
//#endregion

View File

@ -0,0 +1,16 @@
namespace OpenAPI.Model
open System
open System.Collections.Generic
module Tag =
//#region Tag
type Tag = {
Id : int64;
Name : string;
}
//#endregion

View File

@ -0,0 +1,22 @@
namespace OpenAPI.Model
open System
open System.Collections.Generic
module User =
//#region User
type User = {
Id : int64;
Username : string;
FirstName : string;
LastName : string;
Email : string;
Password : string;
Phone : string;
UserStatus : int;
}
//#endregion