From e8a83dfe1f085dfd3446f7e2d00c37573b4a0e77 Mon Sep 17 00:00:00 2001 From: William Cheng Date: Mon, 6 May 2019 01:30:51 +0800 Subject: [PATCH] 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 --- bin/fsharp-giraffe-server-petstore.sh | 32 + .../fsharp-giraffe-server-petstore.bat | 10 + docs/generators.md | 1 + docs/generators/fsharp-giraffe.md | 25 + .../languages/AbstractFSharpCodegen.java | 1154 +++++++++++++++++ .../languages/FsharpGiraffeServerCodegen.java | 287 ++++ .../org.openapitools.codegen.CodegenConfig | 1 + .../AuthSchemes.mustache | 100 ++ .../CustomHandlers.mustache | 112 ++ .../fsharp-giraffe-server/Handler.mustache | 67 + .../HandlerParams.mustache | 147 +++ .../HandlerTests.mustache | 65 + .../HandlerTestsHelper.mustache | 46 + .../fsharp-giraffe-server/Helpers.mustache | 12 + .../fsharp-giraffe-server/Model.mustache | 36 + .../fsharp-giraffe-server/Program.mustache | 100 ++ .../Project.Tests.fsproj.mustache | 32 + .../Project.fsproj.mustache | 44 + .../fsharp-giraffe-server/README.mustache | 198 +++ .../ServiceImpl.mustache | 44 + .../ServiceInterface.mustache | 17 + .../fsharp-giraffe-server/TestHelper.mustache | 83 ++ .../fsharp-giraffe-server/build.bat.mustache | 3 + .../fsharp-giraffe-server/build.sh.mustache | 4 + .../fsharp-giraffe-server/gitignore.mustache | 5 + .../fsharp/FSharpServerCodegenTest.java | 94 ++ .../fsharp-giraffe/.openapi-generator-ignore | 23 + .../fsharp-giraffe/.openapi-generator/VERSION | 1 + .../OpenAPI.Tests/OpenAPITests.fsproj | 32 + .../OpenAPI.Tests/PetApiTests.fs | 295 +++++ .../OpenAPI.Tests/PetApiTestsHelper.fs | 117 ++ .../OpenAPI.Tests/StoreApiTests.fs | 173 +++ .../OpenAPI.Tests/StoreApiTestsHelper.fs | 42 + .../OpenAPI.Tests/TestHelper.fs | 83 ++ .../OpenAPI.Tests/UserApiTests.fs | 272 ++++ .../OpenAPI.Tests/UserApiTestsHelper.fs | 85 ++ .../fsharp-giraffe/OpenAPI/.gitignore | 5 + .../petstore/fsharp-giraffe/OpenAPI/README.md | 186 +++ .../petstore/fsharp-giraffe/OpenAPI/build.bat | 3 + .../petstore/fsharp-giraffe/OpenAPI/build.sh | 4 + .../fsharp-giraffe/OpenAPI/src/OpenAPI.fsproj | 49 + .../fsharp-giraffe/OpenAPI/src/Program.fs | 109 ++ .../OpenAPI/src/api/PetApiHandler.fs | 179 +++ .../OpenAPI/src/api/PetApiHandlerParams.fs | 213 +++ .../OpenAPI/src/api/PetApiServiceInterface.fs | 20 + .../OpenAPI/src/api/StoreApiHandler.fs | 97 ++ .../OpenAPI/src/api/StoreApiHandlerParams.fs | 88 ++ .../src/api/StoreApiServiceInterface.fs | 16 + .../OpenAPI/src/api/UserApiHandler.fs | 173 +++ .../OpenAPI/src/api/UserApiHandlerParams.fs | 169 +++ .../src/api/UserApiServiceInterface.fs | 20 + .../OpenAPI/src/auth/AuthSchemes.fs | 88 ++ .../OpenAPI/src/helpers/Helpers.fs | 12 + .../OpenAPI/src/impl/CustomHandlers.fs | 112 ++ .../OpenAPI/src/impl/PetApiService.fs | 72 + .../OpenAPI/src/impl/StoreApiService.fs | 49 + .../OpenAPI/src/impl/UserApiService.fs | 68 + .../OpenAPI/src/model/ApiResponse.fs | 17 + .../OpenAPI/src/model/Category.fs | 16 + .../OpenAPI/src/model/InlineObject.fs | 16 + .../OpenAPI/src/model/InlineObject1.fs | 16 + .../fsharp-giraffe/OpenAPI/src/model/Order.fs | 23 + .../fsharp-giraffe/OpenAPI/src/model/Pet.fs | 25 + .../fsharp-giraffe/OpenAPI/src/model/Tag.fs | 16 + .../fsharp-giraffe/OpenAPI/src/model/User.fs | 22 + 65 files changed, 5725 insertions(+) create mode 100755 bin/fsharp-giraffe-server-petstore.sh create mode 100644 bin/windows/fsharp-giraffe-server-petstore.bat create mode 100644 docs/generators/fsharp-giraffe.md create mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractFSharpCodegen.java create mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/FsharpGiraffeServerCodegen.java create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/AuthSchemes.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/CustomHandlers.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Handler.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/HandlerParams.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/HandlerTests.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/HandlerTestsHelper.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Helpers.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Model.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Program.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Project.Tests.fsproj.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Project.fsproj.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/README.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/ServiceImpl.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/ServiceInterface.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/TestHelper.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/build.bat.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/build.sh.mustache create mode 100644 modules/openapi-generator/src/main/resources/fsharp-giraffe-server/gitignore.mustache create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/fsharp/FSharpServerCodegenTest.java create mode 100644 samples/server/petstore/fsharp-giraffe/.openapi-generator-ignore create mode 100644 samples/server/petstore/fsharp-giraffe/.openapi-generator/VERSION create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/OpenAPITests.fsproj create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/PetApiTests.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/PetApiTestsHelper.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/StoreApiTests.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/StoreApiTestsHelper.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/TestHelper.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/UserApiTests.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/UserApiTestsHelper.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/.gitignore create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/README.md create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/build.bat create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/build.sh create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/OpenAPI.fsproj create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/Program.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/PetApiHandler.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/PetApiHandlerParams.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/PetApiServiceInterface.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/StoreApiHandler.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/StoreApiHandlerParams.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/StoreApiServiceInterface.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/UserApiHandler.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/UserApiHandlerParams.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/UserApiServiceInterface.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/auth/AuthSchemes.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/helpers/Helpers.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/impl/CustomHandlers.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/impl/PetApiService.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/impl/StoreApiService.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/impl/UserApiService.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/ApiResponse.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/Category.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/InlineObject.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/InlineObject1.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/Order.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/Pet.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/Tag.fs create mode 100644 samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/User.fs diff --git a/bin/fsharp-giraffe-server-petstore.sh b/bin/fsharp-giraffe-server-petstore.sh new file mode 100755 index 000000000000..117b6b7d782e --- /dev/null +++ b/bin/fsharp-giraffe-server-petstore.sh @@ -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} diff --git a/bin/windows/fsharp-giraffe-server-petstore.bat b/bin/windows/fsharp-giraffe-server-petstore.bat new file mode 100644 index 000000000000..f52daf05e655 --- /dev/null +++ b/bin/windows/fsharp-giraffe-server-petstore.bat @@ -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% diff --git a/docs/generators.md b/docs/generators.md index aae6c610bb05..7d7746b508cf 100644 --- a/docs/generators.md +++ b/docs/generators.md @@ -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) diff --git a/docs/generators/fsharp-giraffe.md b/docs/generators/fsharp-giraffe.md new file mode 100644 index 000000000000..473781b7c520 --- /dev/null +++ b/docs/generators/fsharp-giraffe.md @@ -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| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractFSharpCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractFSharpCodegen.java new file mode 100644 index 000000000000..19be68a999d3 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractFSharpCodegen.java @@ -0,0 +1,1154 @@ +/* + * 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.google.common.collect.ImmutableMap; +import com.samskivert.mustache.Mustache; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.Schema; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.openapitools.codegen.*; +import org.openapitools.codegen.templating.mustache.*; +import org.openapitools.codegen.utils.ModelUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.*; + +import static org.openapitools.codegen.utils.StringUtils.camelize; + +public abstract class AbstractFSharpCodegen extends DefaultCodegen implements CodegenConfig { + + protected boolean optionalAssemblyInfoFlag = true; + protected boolean optionalProjectFileFlag = true; + + protected boolean useDateTimeOffsetFlag = false; + protected boolean useCollection = false; + protected boolean returnICollection = false; + protected boolean netCoreProjectFileFlag = false; + + protected String modelPropertyNaming = CodegenConstants.MODEL_PROPERTY_NAMING_TYPE.PascalCase.name(); + + protected String licenseUrl = "http://localhost"; + protected String licenseName = "NoLicense"; + + protected String packageVersion = "1.0.0"; + protected String packageName = "OpenAPI"; + protected String packageTitle = "OpenAPI Library"; + protected String packageProductName = "OpenAPILibrary"; + protected String packageDescription = "A library generated from a OpenAPI doc"; + protected String packageCompany = "OpenAPI"; + protected String packageCopyright = "No Copyright"; + protected String packageAuthors = "OpenAPI"; + + protected String interfacePrefix = "I"; + + protected String projectFolder = packageName; + protected String sourceFolder = projectFolder + File.separator + "src"; + protected String testFolder = projectFolder + ".Tests"; + + protected Set collectionTypes; + protected Set mapTypes; + + // true if nullable types will be supported (as option) + protected boolean supportNullable = Boolean.TRUE; + + protected Set nullableType = new HashSet(); + + + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractFSharpCodegen.class); + + public AbstractFSharpCodegen() { + super(); + + supportsInheritance = true; + + importMapping.clear(); + importMapping.put("IDictionary", "System.Collections.Generic"); + + outputFolder = this.getName(); + embeddedTemplateDir = templateDir = this.getName(); + + collectionTypes = new HashSet(Arrays.asList("list", "seq")); + + mapTypes = new HashSet( + Arrays.asList("IDictionary") + ); + + reservedWords.addAll( + Arrays.asList( + // local variable names in API methods (endpoints) + "localVarPath", "localVarPathParams", "localVarQueryParams", "localVarHeaderParams", + "localVarFormParams", "localVarFileParams", "localVarStatusCode", "localVarResponse", + "localVarPostBody", "localVarHttpHeaderAccepts", "localVarHttpHeaderAccept", + "localVarHttpContentTypes", "localVarHttpContentType", + "localVarStatusCode", + // F# reserved words + "abstract", "and", "as", "async", "await", "assert", "base","begin", "bool", "break", "byte", "case", "catch", "char", "checked", + "class", "const", "continue", "decimal", "default", "delegate", "do", "done", "double", "downcast", "downto", "dynamic", + "elif", "else", "end", "enum", "event", "exception", "explicit", "extern", "false", "finally", "fixed", "float", "for", + "foreach", "fun", "function", "if", "in", "inherit", "inline", "int", "interface", "internal", "is", "lazy", "let", "let!", "lock", + "match", "match!", "member", "module", "mutable", "namespace", "new", "not", "null", "of", "open", "option", "or", "override", "params", + "private", "public", "raise", "rec", "return", "return!", "sealed", "select", "static", "string", "struct", "then", "to", + "true", "try", "type", "upcast", "use", "use!", "val", "void", "volatile", "when", "while", "with", "yield", "yield!") + ); + + // TODO - these are based on C# generator, do we need to add any more? + languageSpecificPrimitives = new HashSet( + Arrays.asList( + "String", + "string", + "bool", + "char", + "decimal", + "int", + "int16", + "int64", + "nativeint", + "unativeint", + "uint16", + "uint32", + "uint64", + "float", + "byte[]", + "ICollection", + "Collection", + "list", + "dict", + "seq", + "Dictionary", + "List", + "DateTime", + "DataTimeOffset", + "Double", + "Int32", + "Int64", + "float", + "float32", + "single", + "double", + "System.IO.Stream", // not really a primitive, we include it to avoid model import + "obj") + ); + + instantiationTypes.put("array", "list"); + instantiationTypes.put("list", "list"); + instantiationTypes.put("map", "IDictionary"); + + + typeMapping = new HashMap(); + typeMapping.put("string", "string"); + typeMapping.put("binary", "byte[]"); + typeMapping.put("ByteArray", "byte[]"); + typeMapping.put("boolean", "bool"); + typeMapping.put("integer", "int"); + typeMapping.put("float", "float"); + typeMapping.put("long", "int64"); + typeMapping.put("double", "double"); + typeMapping.put("number", "decimal"); + typeMapping.put("DateTime", "DateTime"); + typeMapping.put("date", "DateTime"); + typeMapping.put("file", "System.IO.Stream"); + typeMapping.put("array", "list"); + typeMapping.put("list", "list"); + typeMapping.put("map", "IDictionary"); + typeMapping.put("object", "obj"); + typeMapping.put("UUID", "Guid"); + + // nullable type + nullableType = new HashSet( + Arrays.asList("decimal", "bool", "int", "float", "long", "double", "string", "Guid","apiKey") + ); + } + + public void setReturnICollection(boolean returnICollection) { + this.returnICollection = returnICollection; + } + + public void setUseCollection(boolean useCollection) { + this.useCollection = useCollection; + if (useCollection) { + typeMapping.put("array", "seq"); + typeMapping.put("list", "seq"); + + instantiationTypes.put("array", "seq"); + instantiationTypes.put("list", "seq"); + } + } + + public void setNetCoreProjectFileFlag(boolean flag) { + this.netCoreProjectFileFlag = flag; + } + + public void useDateTimeOffset(boolean flag) { + this.useDateTimeOffsetFlag = flag; + if (flag) { + typeMapping.put("DateTime", "DateTimeOffset?"); + } else { + typeMapping.put("DateTime", "DateTime?"); + } + } + + @Override + public void processOpts() { + super.processOpts(); + + // License info + if (additionalProperties.containsKey(CodegenConstants.LICENSE_URL)) { + setLicenseUrl((String) additionalProperties.get(CodegenConstants.LICENSE_URL)); + } else { + additionalProperties.put(CodegenConstants.LICENSE_URL, this.licenseUrl); + } + + if (additionalProperties.containsKey(CodegenConstants.LICENSE_NAME)) { + setLicenseName((String) additionalProperties.get(CodegenConstants.LICENSE_NAME)); + } else { + additionalProperties.put(CodegenConstants.LICENSE_NAME, this.licenseName); + } + + // {{packageVersion}} + if (additionalProperties.containsKey(CodegenConstants.PACKAGE_VERSION)) { + setPackageVersion((String) additionalProperties.get(CodegenConstants.PACKAGE_VERSION)); + } else { + additionalProperties.put(CodegenConstants.PACKAGE_VERSION, packageVersion); + } + + // {{sourceFolder}} + if (additionalProperties.containsKey(CodegenConstants.SOURCE_FOLDER)) { + setSourceFolder((String) additionalProperties.get(CodegenConstants.SOURCE_FOLDER)); + } else { + additionalProperties.put(CodegenConstants.SOURCE_FOLDER, this.sourceFolder); + } + + // {{packageName}} + if (additionalProperties.containsKey(CodegenConstants.PACKAGE_NAME)) { + setPackageName((String) additionalProperties.get(CodegenConstants.PACKAGE_NAME)); + } else { + additionalProperties.put(CodegenConstants.PACKAGE_NAME, packageName); + } + + if (additionalProperties.containsKey(CodegenConstants.INVOKER_PACKAGE)) { + LOGGER.warn(String.format(Locale.ROOT, "%s is not used by F# generators. Please use %s", + CodegenConstants.INVOKER_PACKAGE, CodegenConstants.PACKAGE_NAME)); + } + + // {{packageTitle}} + if (additionalProperties.containsKey(CodegenConstants.PACKAGE_TITLE)) { + setPackageTitle((String) additionalProperties.get(CodegenConstants.PACKAGE_TITLE)); + } else { + additionalProperties.put(CodegenConstants.PACKAGE_TITLE, packageTitle); + } + + // {{packageProductName}} + if (additionalProperties.containsKey(CodegenConstants.PACKAGE_PRODUCTNAME)) { + setPackageProductName((String) additionalProperties.get(CodegenConstants.PACKAGE_PRODUCTNAME)); + } else { + additionalProperties.put(CodegenConstants.PACKAGE_PRODUCTNAME, packageProductName); + } + + // {{packageDescription}} + if (additionalProperties.containsKey(CodegenConstants.PACKAGE_DESCRIPTION)) { + setPackageDescription((String) additionalProperties.get(CodegenConstants.PACKAGE_DESCRIPTION)); + } else { + additionalProperties.put(CodegenConstants.PACKAGE_DESCRIPTION, packageDescription); + } + + // {{packageCompany}} + if (additionalProperties.containsKey(CodegenConstants.PACKAGE_COMPANY)) { + setPackageCompany((String) additionalProperties.get(CodegenConstants.PACKAGE_COMPANY)); + } else { + additionalProperties.put(CodegenConstants.PACKAGE_COMPANY, packageCompany); + } + + // {{packageCopyright}} + if (additionalProperties.containsKey(CodegenConstants.PACKAGE_COPYRIGHT)) { + setPackageCopyright((String) additionalProperties.get(CodegenConstants.PACKAGE_COPYRIGHT)); + } else { + additionalProperties.put(CodegenConstants.PACKAGE_COPYRIGHT, packageCopyright); + } + + // {{packageAuthors}} + if (additionalProperties.containsKey(CodegenConstants.PACKAGE_AUTHORS)) { + setPackageAuthors((String) additionalProperties.get(CodegenConstants.PACKAGE_AUTHORS)); + } else { + additionalProperties.put(CodegenConstants.PACKAGE_AUTHORS, packageAuthors); + } + + // {{useDateTimeOffset}} + if (additionalProperties.containsKey(CodegenConstants.USE_DATETIME_OFFSET)) { + useDateTimeOffset(convertPropertyToBooleanAndWriteBack(CodegenConstants.USE_DATETIME_OFFSET)); + } else { + additionalProperties.put(CodegenConstants.USE_DATETIME_OFFSET, useDateTimeOffsetFlag); + } + + if (additionalProperties.containsKey(CodegenConstants.USE_COLLECTION)) { + setUseCollection(convertPropertyToBooleanAndWriteBack(CodegenConstants.USE_COLLECTION)); + } else { + additionalProperties.put(CodegenConstants.USE_COLLECTION, useCollection); + } + + if (additionalProperties.containsKey(CodegenConstants.RETURN_ICOLLECTION)) { + setReturnICollection(convertPropertyToBooleanAndWriteBack(CodegenConstants.RETURN_ICOLLECTION)); + } else { + additionalProperties.put(CodegenConstants.RETURN_ICOLLECTION, returnICollection); + } + + if (additionalProperties.containsKey(CodegenConstants.NETCORE_PROJECT_FILE)) { + setNetCoreProjectFileFlag(convertPropertyToBooleanAndWriteBack(CodegenConstants.NETCORE_PROJECT_FILE)); + } else { + additionalProperties.put(CodegenConstants.NETCORE_PROJECT_FILE, netCoreProjectFileFlag); + } + + if (additionalProperties.containsKey(CodegenConstants.INTERFACE_PREFIX)) { + String useInterfacePrefix = additionalProperties.get(CodegenConstants.INTERFACE_PREFIX).toString(); + if ("false".equals(useInterfacePrefix.toLowerCase(Locale.ROOT))) { + setInterfacePrefix(""); + } else if (!"true".equals(useInterfacePrefix.toLowerCase(Locale.ROOT))) { + // NOTE: if user passes "true" explicitly, we use the default I- prefix. The other supported case here is a custom prefix. + setInterfacePrefix(sanitizeName(useInterfacePrefix)); + } + } + + // This either updates additionalProperties with the above fixes, or sets the default if the option was not specified. + additionalProperties.put(CodegenConstants.INTERFACE_PREFIX, interfacePrefix); + + addMustacheLambdas(additionalProperties); + } + + private void addMustacheLambdas(Map objs) { + + Map lambdas = new ImmutableMap.Builder() + .put("lowercase", new LowercaseLambda().generator(this)) + .put("uppercase", new UppercaseLambda()) + .put("titlecase", new TitlecaseLambda()) + .put("camelcase", new CamelCaseLambda().generator(this)) + .put("camelcase_param", new CamelCaseLambda().generator(this).escapeAsParamName(true)) + .put("indented", new IndentedLambda()) + .put("indented_8", new IndentedLambda(8, " ")) + .put("indented_12", new IndentedLambda(12, " ")) + .put("indented_16", new IndentedLambda(16, " ")) + .build(); + + if (objs.containsKey("lambda")) { + LOGGER.warn("A property named 'lambda' already exists. Mustache lambdas renamed from 'lambda' to '_lambda'. " + + "You'll likely need to use a custom template, " + + "see https://github.com/swagger-api/swagger-codegen#modifying-the-client-library-format. "); + objs.put("_lambda", lambdas); + } else { + objs.put("lambda", lambdas); + } + } + + @Override + public Map postProcessModels(Map objs) { + super.postProcessModels(objs); + List models = (List) objs.get("models"); + for (Object _mo : models) { + Map mo = (Map) _mo; + CodegenModel cm = (CodegenModel) mo.get("model"); + for (CodegenProperty var : cm.vars) { + // check to see if model name is same as the property name + // which will result in compilation error + // if found, prepend with _ to workaround the limitation + if (var.name.equalsIgnoreCase(cm.name)) { + var.name = "_" + var.name; + } + } + } + // process enum in models + return postProcessModelsEnum(objs); + } + + /** + * Invoked by {@link DefaultGenerator} after all models have been post-processed, allowing for a last pass of codegen-specific model cleanup. + * + * @param objs Current state of codegen object model. + * @return (ew) modified state of the codegen object model. + */ + @Override + public Map postProcessAllModels(Map objs) { + final Map processed = super.postProcessAllModels(objs); + postProcessEnumRefs(processed); + return postProcessDependencyOrders(processed); + } + + /* + * F# does not allow forward declarations, so files must be imported in the correct order. + * Output of CodeGen models must therefore bein dependency order (rather than alphabetical order, which seems to be the default). + * We achieve this by creating a comparator to check whether the first model contains any properties of the comparison model's type + * This could probably be made more efficient if absolutely needed. + */ + @SuppressWarnings({"unchecked"}) + public Map postProcessDependencyOrders(final Map objs) { + Comparator comparator = new Comparator() { + @Override public int compare(String key1, String key2) { + // Get the corresponding models + CodegenModel model1 = ModelUtils.getModelByName(key1, objs); + CodegenModel model2 = ModelUtils.getModelByName(key2, objs); + + List complexVars1 = new ArrayList(); + List complexVars2 = new ArrayList(); + + for(CodegenProperty prop : model1.vars) { + if(prop.complexType != null) + complexVars1.add(prop.complexType); + } + for(CodegenProperty prop : model2.vars) { + if(prop.complexType != null) + complexVars2.add(prop.complexType); + } + + // if first has complex vars and second has none, first is greater + if(complexVars1.size() > 0 && complexVars2.size() == 0) + return 1; + + // if second has complex vars and first has none, first is lesser + if(complexVars1.size() == 0 && complexVars2.size() > 0) + return -1; + + // if first has complex var that matches the second's key, first is greater + if(complexVars1.contains(key2)) + return 1; + + // if second has complex var that matches the first's key, first is lesser + if(complexVars2.contains(key1)) + return -1; + + // if none of the above, don't care + return 0; + + } + }; + PriorityQueue queue = new PriorityQueue(objs.size(), comparator); + for(Object k : objs.keySet()) { + queue.add(k.toString()); + } + + Map sorted = new LinkedHashMap(); + + while(queue.size() > 0) { + String key = queue.poll(); + sorted.put(key, objs.get(key)); + } + return sorted; + } + + /** + * F# differs from other languages in that Enums are not _true_ objects; enums are compiled to integral types. + * So, in F#, an enum is considers more like a user-defined primitive. + *

+ * When working with enums, we can't always assume a RefModel is a nullable type (where default(YourType) == null), + * so this post processing runs through all models to find RefModel'd enums. Then, it runs through all vars and modifies + * those vars referencing RefModel'd enums to work the same as inlined enums rather than as objects. + * + * @param models processed models to be further processed for enum references + */ + @SuppressWarnings({"unchecked"}) + private void postProcessEnumRefs(final Map models) { + Map enumRefs = new HashMap(); + for (Map.Entry entry : models.entrySet()) { + CodegenModel model = ModelUtils.getModelByName(entry.getKey(), models); + if (model.isEnum) { + enumRefs.put(entry.getKey(), model); + } + } + + for (Map.Entry entry : models.entrySet()) { + String openAPIName = entry.getKey(); + CodegenModel model = ModelUtils.getModelByName(openAPIName, models); + if (model != null) { + for (CodegenProperty var : model.allVars) { + if (enumRefs.containsKey(var.dataType)) { + // Handle any enum properties referred to by $ref. + // This is different in F# than most other generators, because enums in C# are compiled to integral types, + // while enums in many other languages are true objects. + CodegenModel refModel = enumRefs.get(var.dataType); + var.allowableValues = refModel.allowableValues; + var.isEnum = true; + + // We do these after updateCodegenPropertyEnum to avoid generalities that don't mesh with C#. + var.isPrimitiveType = true; + } + } + + // We're looping all models here. + if (model.isEnum) { + // We now need to make allowableValues.enumVars look like the context of CodegenProperty + Boolean isString = false; + Boolean isInteger = false; + Boolean isLong = false; + Boolean isByte = false; + + if (model.dataType.startsWith("byte")) { + // F# Actually supports byte and short enums, swagger spec only supports byte. + isByte = true; + model.vendorExtensions.put("x-enum-byte", true); + } else if (model.dataType.startsWith("int32")) { + isInteger = true; + model.vendorExtensions.put("x-enum-integer", true); + } else if (model.dataType.startsWith("int64")) { + isLong = true; + model.vendorExtensions.put("x-enum-long", true); + } else { + // F# doesn't support non-integral enums, so we need to treat everything else as strings (e.g. to not lose precision or data integrity) + isString = true; + model.vendorExtensions.put("x-enum-string", true); + } + + // Since we iterate enumVars for modelnnerEnum and enumClass templates, and CodegenModel is missing some of CodegenProperty's properties, + // we can take advantage of Mustache's contextual lookup to add the same "properties" to the model's enumVars scope rather than CodegenProperty's scope. + List> enumVars = (ArrayList>) model.allowableValues.get("enumVars"); + List> newEnumVars = new ArrayList>(); + for (Map enumVar : enumVars) { + Map mixedVars = new HashMap(); + mixedVars.putAll(enumVar); + + mixedVars.put("isString", isString); + mixedVars.put("isLong", isLong); + mixedVars.put("isInteger", isInteger); + mixedVars.put("isByte", isByte); + + newEnumVars.add(mixedVars); + } + + if (!newEnumVars.isEmpty()) { + model.allowableValues.put("enumVars", newEnumVars); + } + } + } else { + LOGGER.warn("Expected to retrieve model %s by name, but no model was found. Check your -Dmodels inclusions.", openAPIName); + } + } + } + + /** + * Update codegen property's enum by adding "enumVars" (with name and value) + * + * @param var list of CodegenProperty + */ + @Override + public void updateCodegenPropertyEnum(CodegenProperty var) { + if (var.vendorExtensions == null) { + var.vendorExtensions = new HashMap<>(); + } + + super.updateCodegenPropertyEnum(var); + + // Because C# uses nullable primitives for datatype, and datatype is used in DefaultCodegen for determining enum-ness, guard against weirdness here. + if (var.isEnum) { + if ("byte".equals(var.dataFormat)) {// C# Actually supports byte and short enums. + var.vendorExtensions.put("x-enum-byte", true); + var.isString = false; + var.isLong = false; + var.isInteger = false; + } else if ("int32".equals(var.dataFormat)) { + var.isInteger = true; + var.isString = false; + var.isLong = false; + } else if ("int64".equals(var.dataFormat)) { + var.isLong = true; + var.isString = false; + var.isInteger = false; + } else {// C# doesn't support non-integral enums, so we need to treat everything else as strings (e.g. to not lose precision or data integrity) + var.isString = true; + var.isInteger = false; + var.isLong = false; + } + } + } + + @Override + public Map postProcessOperationsWithModels(Map objs, List allModels) { + super.postProcessOperationsWithModels(objs, allModels); + if (objs != null) { + Map operations = (Map) objs.get("operations"); + if (operations != null) { + List ops = (List) operations.get("operation"); + for (CodegenOperation operation : ops) { + + // Check return types for collection + if (operation.returnType != null) { + String typeMapping; + int namespaceEnd = operation.returnType.lastIndexOf("."); + if (namespaceEnd > 0) { + typeMapping = operation.returnType.substring(namespaceEnd); + } else { + typeMapping = operation.returnType; + } + + if (this.collectionTypes.contains(typeMapping)) { + operation.isListContainer = true; + operation.returnContainer = operation.returnType; + if (this.returnICollection && ( + typeMapping.startsWith("List") || + typeMapping.startsWith("Collection"))) { + // NOTE: ICollection works for both List and Collection + int genericStart = typeMapping.indexOf("<"); + if (genericStart > 0) { + operation.returnType = "ICollection" + typeMapping.substring(genericStart); + } + } + } else { + operation.returnContainer = operation.returnType; + operation.isMapContainer = this.mapTypes.contains(typeMapping); + } + } + + if (operation.examples != null) { + for (Map example : operation.examples) { + for (Map.Entry entry : example.entrySet()) { + // Replace " with \", \r, \n with \\r, \\n + String val = entry.getValue().replace("\"", "\\\"") + .replace("\r", "\\r") + .replace("\n", "\\n"); + entry.setValue(val); + } + } + } + + if (!isSupportNullable()) { + for (CodegenParameter parameter : operation.allParams) { + CodegenModel model = null; + for (Object modelHashMap : allModels) { + CodegenModel codegenModel = ((HashMap) modelHashMap).get("model"); + if (codegenModel.getClassname().equals(parameter.dataType)) { + model = codegenModel; + break; + } + } + + if (model == null) { + // Primitive data types all come already marked + parameter.isNullable = true; + } else { + // Effectively mark enum models as enums and non-nullable + if (model.isEnum) { + parameter.isEnum = true; + parameter.allowableValues = model.allowableValues; + parameter.isPrimitiveType = true; + parameter.isNullable = false; + } else { + parameter.isNullable = true; + } + } + } + } + + processOperation(operation); + } + } + } + + return objs; + } + + protected void processOperation(CodegenOperation operation) { + // default noop + } + + @Override + public String apiFileFolder() { + return outputFolder + File.separator + sourceFolder + File.separator + apiPackage(); + } + + @Override + public String modelFileFolder() { + return outputFolder + File.separator + sourceFolder + File.separator + modelPackage(); + } + + @Override + public String toModelFilename(String name) { + // should be the same as the model name + return toModelName(name); + } + + @Override + public String toOperationId(String operationId) { + // throw exception if method name is empty (should not occur as an auto-generated method name will be used) + if (StringUtils.isEmpty(operationId)) { + throw new RuntimeException("Empty method name (operationId) not allowed"); + } + + // method name cannot use reserved keyword, e.g. return + if (isReservedWord(operationId)) { + LOGGER.warn(operationId + " (reserved word) cannot be used as method name. Renamed to " + camelize(sanitizeName("call_" + operationId))); + operationId = "call_" + operationId; + } + + // operationId starts with a number + if (operationId.matches("^\\d.*")) { + LOGGER.warn(operationId + " (starting with a number) cannot be used as method name. Renamed to " + camelize(sanitizeName("call_" + operationId))); + operationId = "call_" + operationId; + } + + return camelize(sanitizeName(operationId)); + } + + @Override + public String toVarName(String name) { + // sanitize name + name = sanitizeName(name); + + // if it's all uppper case, do nothing + if (name.matches("^[A-Z_]*$")) { + return name; + } + + // camelize the variable name + // pet_id => PetId + name = camelize(name); + // for reserved word or word starting with number, append _ + if (isReservedWord(name) || name.matches("^\\d.*")) { + name = escapeReservedWord(name); + } + return name; + } + + @Override + public String toParamName(String name) { + // sanitize name + name = sanitizeName(name); + + // replace - with _ e.g. created-at => created_at + name = name.replaceAll("-", "_"); + + // if it's all uppper case, do nothing + if (name.matches("^[A-Z_]*$")) { + return name; + } + + // camelize(lower) the variable name + // pet_id => petId + name = camelize(name, true); + + // for reserved word or word starting with number, append _ + if (isReservedWord(name) || name.matches("^\\d.*")) { + name = escapeReservedWord(name); + } + + return name; + } + + @Override + public String escapeReservedWord(String name) { + if (this.reservedWordsMappings().containsKey(name)) { + return this.reservedWordsMappings().get(name); + } + return "_" + name; + } + + /** + * Return the example value of the property + * + * @param p OpenAPI property object + * @return string presentation of the example value of the property + */ + @Override + public String toExampleValue(Schema p) { + if (ModelUtils.isStringSchema(p)) { + if (p.getExample() != null) { + return "\"" + p.getExample().toString() + "\""; + } + } else if (ModelUtils.isBooleanSchema(p)) { + if (p.getExample() != null) { + return p.getExample().toString(); + } + } else if (ModelUtils.isDateSchema(p)) { + // TODO + } else if (ModelUtils.isDateTimeSchema(p)) { + // TODO + } else if (ModelUtils.isNumberSchema(p)) { + if (p.getExample() != null) { + return p.getExample().toString(); + } + } else if (ModelUtils.isIntegerSchema(p)) { + if (p.getExample() != null) { + return p.getExample().toString(); + } + } + + return null; + } + + /** + * Return the default value of the property + * @param p OpenAPI property object + * @return string presentation of the default value of the property + */ + @Override + public String toDefaultValue(Schema p) { + if (ModelUtils.isBooleanSchema(p)) { + if (p.getDefault() != null) { + return p.getDefault().toString(); + } + } else if (ModelUtils.isDateSchema(p)) { + if (p.getDefault() != null) { + return "\"" + p.getDefault().toString() + "\""; + } + } else if (ModelUtils.isDateTimeSchema(p)) { + if (p.getDefault() != null) { + return "\"" + p.getDefault().toString() + "\""; + } + } else if (ModelUtils.isNumberSchema(p)) { + if (p.getDefault() != null) { + if (ModelUtils.isFloatSchema(p)) { // float + return p.getDefault().toString() + "F"; + } else if (ModelUtils.isDoubleSchema(p)) { // double + return p.getDefault().toString() + "D"; + } else { // decimal + return p.getDefault().toString() + "M"; + } + } + } else if (ModelUtils.isIntegerSchema(p)) { + if (p.getDefault() != null) { + return p.getDefault().toString(); + } + } else if (ModelUtils.isStringSchema(p)) { + if (p.getDefault() != null) { + String _default = (String) p.getDefault(); + if (p.getEnum() == null) { + return "\"" + _default + "\""; + } else { + // convert to enum var name later in postProcessModels + return _default; + } + } + } + + return null; + } + + @Override + protected boolean isReservedWord(String word) { + return reservedWords.contains(word); + } + + + public String getNullableType(Schema p, String type) { + if (languageSpecificPrimitives.contains(type)) { + if (isSupportNullable() && ModelUtils.isNullable(p) && nullableType.contains(type)) { + return type + " option"; + } else { + return type; + } + } else { + return null; + } + } + + @Override + public String getSchemaType(Schema p) { + String openAPIType = super.getSchemaType(p); + String type; + + if (openAPIType == null) { + LOGGER.error("OpenAPI Type for {} is null. Default to UNKNOWN_OPENAPI_TYPE instead.", p.getName()); + openAPIType = "UNKNOWN_OPENAPI_TYPE"; + } + + if (typeMapping.containsKey(openAPIType)) { + type = typeMapping.get(openAPIType); + String languageType = getNullableType(p, type); + if (languageType != null) { + return languageType; + } + } else { + type = openAPIType; + } + + return toModelName(type); + } + + /** + * Provides F# strongly typed declaration for simple arrays of some type and arrays of arrays of some type. + * + * @param arr The input array property + * @return The type declaration when the type is an array of arrays. + */ + private String getArrayTypeDeclaration(ArraySchema arr) { + // TODO: collection type here should be fully qualified namespace to avoid model conflicts + // This supports arrays of arrays. + String arrayType = typeMapping.get("array"); + StringBuilder instantiationType = new StringBuilder(arrayType); + Schema items = arr.getItems(); + String nestedType = getTypeDeclaration(items); + // TODO: We may want to differentiate here between generics and primitive arrays. + return nestedType + "[]"; + } + + @Override + public String toInstantiationType(Schema p) { + if (ModelUtils.isArraySchema(p)) { + return getArrayTypeDeclaration((ArraySchema) p); + } + return super.toInstantiationType(p); + } + + @Override + public String getTypeDeclaration(Schema p) { + if (ModelUtils.isArraySchema(p)) { + return getArrayTypeDeclaration((ArraySchema) p); + } else if (ModelUtils.isMapSchema(p)) { + // Should we also support maps of maps? + Schema inner = ModelUtils.getAdditionalProperties(p); + return getSchemaType(p) + ""; + } + return super.getTypeDeclaration(p); + } + + @Override + public String toModelName(String name) { + if (!StringUtils.isEmpty(modelNamePrefix)) { + name = modelNamePrefix + "_" + name; + } + + if (!StringUtils.isEmpty(modelNameSuffix)) { + name = name + "_" + modelNameSuffix; + } + + name = sanitizeName(name); + + // model name cannot use reserved keyword, e.g. return + if (isReservedWord(name)) { + LOGGER.warn(name + " (reserved word) cannot be used as model name. Renamed to " + camelize("model_" + name)); + name = "model_" + name; // e.g. return => ModelReturn (after camelize) + } + + // model name starts with number + if (name.matches("^\\d.*")) { + LOGGER.warn(name + " (model name starts with number) cannot be used as model name. Renamed to " + camelize("model_" + name)); + name = "model_" + name; // e.g. 200Response => Model200Response (after camelize) + } + + // camelize the model name + // phone_number => PhoneNumber + return camelize(name); + } + + @Override + public String apiTestFileFolder() { + return outputFolder + File.separator + testFolder; + } + + @Override + public String modelTestFileFolder() { + return outputFolder + File.separator + testFolder; + } + + @Override + public String toApiTestFilename(String name) { + return toApiName(name) + "Tests"; + } + + @Override + public String toModelTestFilename(String name) { + return toModelName(name) + "Tests"; + } + + public void setLicenseUrl(String licenseUrl) {this.licenseUrl = licenseUrl;} + + public void setLicenseName(String licenseName) {this.licenseName = licenseName;} + + public void setPackageName(String packageName) { + this.packageName = packageName; + this.projectFolder = packageName; + this.sourceFolder = projectFolder + File.separator + "src"; + this.testFolder = projectFolder + ".Tests"; + } + + public void setPackageVersion(String packageVersion) { + this.packageVersion = packageVersion; + } + + public void setPackageTitle(String packageTitle) { + this.packageTitle = packageTitle; + } + + public void setPackageProductName(String packageProductName) { + this.packageProductName = packageProductName; + } + + public void setPackageDescription(String packageDescription) { + this.packageDescription = packageDescription; + } + + public void setPackageCompany(String packageCompany) { + this.packageCompany = packageCompany; + } + + public void setPackageCopyright(String packageCopyright) { + this.packageCopyright = packageCopyright; + } + + public void setPackageAuthors(String packageAuthors) { + this.packageAuthors = packageAuthors; + } + + public void setSourceFolder(String sourceFolder) { + this.sourceFolder = sourceFolder; + } + + public String getInterfacePrefix() { + return interfacePrefix; + } + + public void setInterfacePrefix(final String interfacePrefix) { + this.interfacePrefix = interfacePrefix; + } + + public boolean isSupportNullable() { + return supportNullable; + } + + public void setSupportNullable(final boolean supportNullable) { + this.supportNullable = supportNullable; + } + + @Override + public String toEnumValue(String value, String datatype) { + // C# only supports enums as literals for int, int?, long, long?, byte, and byte?. All else must be treated as strings. + // Per: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/enum + // The approved types for an enum are byte, sbyte, short, ushort, int, uint, long, or ulong. + // but we're not supporting unsigned integral types or shorts. + if (datatype.startsWith("int") || datatype.startsWith("long") || datatype.startsWith("byte")) { + return value; + } + + return escapeText(value); + } + + @Override + public String toEnumVarName(String name, String datatype) { + if (name.length() == 0) { + return "Empty"; + } + + // for symbol, e.g. $, # + if (getSymbolName(name) != null) { + return camelize(getSymbolName(name)); + } + + String enumName = sanitizeName(name); + + enumName = enumName.replaceFirst("^_", ""); + enumName = enumName.replaceFirst("_$", ""); + + enumName = camelize(enumName) + "Enum"; + + if (enumName.matches("\\d.*")) { // starts with number + return "_" + enumName; + } else { + return enumName; + } + } + + @Override + public String toEnumName(CodegenProperty property) { + return sanitizeName(camelize(property.name)) + "Enum"; + } + + public String testPackageName() { + return this.packageName + ".Test"; + } + + @Override + public String escapeQuotationMark(String input) { + // remove " to avoid code injection + return input.replace("\"", ""); + } + + @Override + public String escapeUnsafeCharacters(String input) { + return input.replace("*/", "*_/").replace("/*", "/_*").replace("--", "- -"); + } + + @Override + public boolean isDataTypeString(String dataType) { + // also treat double/decimal/float as "string" in enum so that the values (e.g. 2.8) get double-quoted + return "String".equalsIgnoreCase(dataType) || + "double?".equals(dataType) || "decimal?".equals(dataType) || "float?".equals(dataType) || + "double".equals(dataType) || "decimal".equals(dataType) || "float".equals(dataType); + } + + @Override + public void setParameterExampleValue(CodegenParameter codegenParameter) { + + // set the example value + // if not specified in x-example, generate a default value + // TODO need to revise how to obtain the example value + if (codegenParameter.vendorExtensions != null && codegenParameter.vendorExtensions.containsKey("x-example")) { + codegenParameter.example = Json.pretty(codegenParameter.vendorExtensions.get("x-example")); + } else if (Boolean.TRUE.equals(codegenParameter.isBoolean)) { + codegenParameter.example = "true"; + } else if (Boolean.TRUE.equals(codegenParameter.isLong)) { + codegenParameter.example = "789"; + } else if (Boolean.TRUE.equals(codegenParameter.isInteger)) { + codegenParameter.example = "56"; + } else if (Boolean.TRUE.equals(codegenParameter.isFloat)) { + codegenParameter.example = "3.4F"; + } else if (Boolean.TRUE.equals(codegenParameter.isDouble)) { + codegenParameter.example = "1.2D"; + } else if (Boolean.TRUE.equals(codegenParameter.isNumber)) { + codegenParameter.example = "8.14"; + } else if (Boolean.TRUE.equals(codegenParameter.isBinary)) { + codegenParameter.example = "BINARY_DATA_HERE"; + } else if (Boolean.TRUE.equals(codegenParameter.isByteArray)) { + codegenParameter.example = "BYTE_ARRAY_DATA_HERE"; + } else if (Boolean.TRUE.equals(codegenParameter.isFile)) { + codegenParameter.example = "/path/to/file.txt"; + } else if (Boolean.TRUE.equals(codegenParameter.isDate)) { + codegenParameter.example = "2013-10-20"; + } else if (Boolean.TRUE.equals(codegenParameter.isDateTime)) { + codegenParameter.example = "2013-10-20T19:20:30+01:00"; + } else if (Boolean.TRUE.equals(codegenParameter.isUuid)) { + codegenParameter.example = "38400000-8cf0-11bd-b23e-10b96e4ef00d"; + } else if (Boolean.TRUE.equals(codegenParameter.isString)) { + codegenParameter.example = codegenParameter.paramName + "_example"; + } + } + + @Override + public void postProcessFile(File file, String fileType) { + if (file == null) { + return; + } + + String fsharpPostProcessFile = System.getenv("FSHARP_POST_PROCESS_FILE"); + if (StringUtils.isEmpty(fsharpPostProcessFile)) { + return; // skip if FSHARP_POST_PROCESS_FILE env variable is not defined + } + + // only process files with .fs extension + if ("fs".equals(FilenameUtils.getExtension(file.toString()))) { + String command = fsharpPostProcessFile + " " + file.toString(); + try { + Process p = Runtime.getRuntime().exec(command); + int exitValue = p.waitFor(); + if (exitValue != 0) { + LOGGER.error("Error running the command ({}). Exit code: {}", command, exitValue); + } else { + LOGGER.info("Successfully executed: " + command); + } + } catch (Exception e) { + LOGGER.error("Error running the command ({}). Exception: {}", command, e.getMessage()); + } + } + } +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/FsharpGiraffeServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/FsharpGiraffeServerCodegen.java new file mode 100644 index 000000000000..eb9708bc119b --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/FsharpGiraffeServerCodegen.java @@ -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 postProcessSupportingFileData(Map 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); + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig index 24b65ad72c32..4eddf1ebea6f 100644 --- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig +++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -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 diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/AuthSchemes.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/AuthSchemes.mustache new file mode 100644 index 000000000000..cabb34132a61 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/AuthSchemes.mustache @@ -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() + 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 + + \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/CustomHandlers.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/CustomHandlers.mustache new file mode 100644 index 000000000000..cc176cdb66ed --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/CustomHandlers.mustache @@ -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(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) = + () diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Handler.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Handler.mustache new file mode 100644 index 000000000000..908d0faf5bfe --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Handler.mustache @@ -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}} + /// + /// {{description}} + /// + + {{#operation}} + //#region {{operationId}} + /// + /// {{#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}} + + diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/HandlerParams.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/HandlerParams.mustache new file mode 100644 index 000000000000..336ef2d55cf3 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/HandlerParams.mustache @@ -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 + [] + type {{operationId}}PathParams = { + {{/-first}} + {{paramName}} : {{dataType}} {{^required}}option{{/required}}; + {{#-last}} + } + {{/-last}} + //#endregion + {{/pathParams}} + {{#queryParams}} + + {{#-first}} + //#region Query parameters + [] + type {{operationId}}QueryParams = { + {{/-first}} + {{paramName}} : {{dataType}} {{^required}}option{{/required}}; + + {{#-last}} + } + //#endregion + {{/-last}} + {{/queryParams}} + {{#bodyParams}} + + {{#-first}} + //#region Body parameters + [] + {{^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}} + [] + type {{operationId}}FormParams = { + {{/-first}} + {{paramName}} : {{dataType}} {{^required}}option{{/required}}; + {{#-last}} + } + {{/-last}} + //#endregion + {{/formParams}} + {{#headerParams}} + + //#region Header parameters + {{#-first}} + [] + 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}} + \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/HandlerTests.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/HandlerTests.mustache new file mode 100644 index 000000000000..606bdb995c10 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/HandlerTests.mustache @@ -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}} + [] + 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({{code}})) + |> readText + |> shouldEqual "TESTME" + {{/hasConsumes}} + {{^hasConsumes}} + {{httpMethod}} client path + |> isStatus (enum({{code}})) + |> readText + |> shouldEqual "TESTME" + |> ignore + {{/hasConsumes}} + } + + {{/responses}} + {{/operation}} + {{/operations}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/HandlerTestsHelper.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/HandlerTestsHelper.mustache new file mode 100644 index 000000000000..855b0771c286 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/HandlerTestsHelper.mustache @@ -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}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Helpers.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Helpers.mustache new file mode 100644 index 000000000000..6dcbe4ad94c0 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Helpers.mustache @@ -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 \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Model.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Model.mustache new file mode 100644 index 000000000000..de115478e262 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Model.mustache @@ -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}} + \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Program.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Program.mustache new file mode 100644 index 000000000000..74f0f7b2af96 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Program.mustache @@ -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 + + [] + let main _ = + let builder = WebHost.CreateDefaultBuilder() + .Configure(Action configureApp) + .ConfigureServices(configureServices) + .ConfigureLogging(configureLogging) + |> CustomHandlers.configureWebHost + builder.Build() + .Run() + 0 \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Project.Tests.fsproj.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Project.Tests.fsproj.mustache new file mode 100644 index 000000000000..e1179afc5262 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Project.Tests.fsproj.mustache @@ -0,0 +1,32 @@ + + + + netcoreapp2.2 + {{packageName}}.Tests + portable + + + + + + + + + + + + + + + + + + {{#apiInfo}} + {{#apis}} + + + {{/apis}} + {{/apiInfo}} + + + \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Project.fsproj.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Project.fsproj.mustache new file mode 100644 index 000000000000..0576d996d867 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/Project.fsproj.mustache @@ -0,0 +1,44 @@ + + + {{packageName}} + {{packageName}} + netcoreapp2.2 + portable + false + $(MSBuildThisFileDirectory) + {{packageName}} + {{packageName}} + + + + + + + + + + + + + + {{#models}} + {{#model}} + + {{/model}} + {{/models}} + {{#apiInfo}} + {{#apis}} + {{#operations}} + + + + + {{/operations}} + {{/apis}} + {{/apiInfo}} + + + + + + \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/README.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/README.mustache new file mode 100644 index 000000000000..21d41400b228 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/README.mustache @@ -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. + +## 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. \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/ServiceImpl.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/ServiceImpl.mustache new file mode 100644 index 000000000000..19d5ba0d8777 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/ServiceImpl.mustache @@ -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 \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/ServiceInterface.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/ServiceInterface.mustache new file mode 100644 index 000000000000..68b14c67c264 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/ServiceInterface.mustache @@ -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 \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/TestHelper.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/TestHelper.mustache new file mode 100644 index 000000000000..c8437d7002ee --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/TestHelper.mustache @@ -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 {{packageName}}.App.configureApp) + .ConfigureServices(Action {{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) \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/build.bat.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/build.bat.mustache new file mode 100644 index 000000000000..5c6ac5b1d175 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/build.bat.mustache @@ -0,0 +1,3 @@ +dotnet restore src/{{packageName}}.fsproj +dotnet build src/{{packageName}}.fsproj + diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/build.sh.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/build.sh.mustache new file mode 100644 index 000000000000..2a97c479bc9e --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/build.sh.mustache @@ -0,0 +1,4 @@ +#!/bin/sh +dotnet restore src/{{packageName}}.fsproj +dotnet build src/{{packageName}}.fsproj + diff --git a/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/gitignore.mustache b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/gitignore.mustache new file mode 100644 index 000000000000..8d6278d820fe --- /dev/null +++ b/modules/openapi-generator/src/main/resources/fsharp-giraffe-server/gitignore.mustache @@ -0,0 +1,5 @@ +**/node_modules +**/bin/ +**/obj/ +**/dist/ +**/web.config \ No newline at end of file diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/fsharp/FSharpServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/fsharp/FSharpServerCodegenTest.java new file mode 100644 index 000000000000..53df26bb8069 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/fsharp/FSharpServerCodegenTest.java @@ -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 models = new HashMap(); + 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 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 { + + } +} diff --git a/samples/server/petstore/fsharp-giraffe/.openapi-generator-ignore b/samples/server/petstore/fsharp-giraffe/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/.openapi-generator-ignore @@ -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 diff --git a/samples/server/petstore/fsharp-giraffe/.openapi-generator/VERSION b/samples/server/petstore/fsharp-giraffe/.openapi-generator/VERSION new file mode 100644 index 000000000000..afa636560641 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/.openapi-generator/VERSION @@ -0,0 +1 @@ +4.0.0-SNAPSHOT \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/OpenAPITests.fsproj b/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/OpenAPITests.fsproj new file mode 100644 index 000000000000..a89851bcf145 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/OpenAPITests.fsproj @@ -0,0 +1,32 @@ + + + + netcoreapp2.2 + OpenAPI.Tests + portable + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/PetApiTests.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/PetApiTests.fs new file mode 100644 index 000000000000..8408a25602ee --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/PetApiTests.fs @@ -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 + // --------------------------------- + + [] + 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(405)) + |> readText + |> shouldEqual "TESTME" + } + + [] + 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(400)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(200)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(400)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(200)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(400)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(200)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(400)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(404)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(400)) + |> readText + |> shouldEqual "TESTME" + } + + [] + 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(404)) + |> readText + |> shouldEqual "TESTME" + } + + [] + 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(405)) + |> readText + |> shouldEqual "TESTME" + } + + [] + 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(405)) + |> readText + |> shouldEqual "TESTME" + } + + [] + 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(200)) + |> readText + |> shouldEqual "TESTME" + } + diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/PetApiTestsHelper.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/PetApiTestsHelper.fs new file mode 100644 index 000000000000..37fa8b6eb825 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/PetApiTestsHelper.fs @@ -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 "{ + "photoUrls" : [ "photoUrls", "photoUrls" ], + "name" : "doggie", + "id" : 0, + "category" : { + "name" : "name", + "id" : 6 + }, + "tags" : [ { + "name" : "name", + "id" : 1 + }, { + "name" : "name", + "id" : 1 + } ], + "status" : "available" +}" + AddPetExamples <- AddPetExamples.Add("application/json", AddPetBody) + + AddPetBody <- WebUtility.HtmlDecode "<Pet> + <id>123456789</id> + <name>doggie</name> + <photoUrls> + <photoUrls>aeiou</photoUrls> + </photoUrls> + <tags> + </tags> + <status>aeiou</status> +</Pet>" + AddPetExamples <- AddPetExamples.Add("application/xml", AddPetBody) + + let getAddPetExample mediaType = + AddPetExamples.[mediaType] + |> getConverter mediaType + () + + () + + () + + () + + + let mutable UpdatePetExamples = Map.empty + let mutable UpdatePetBody = "" + + UpdatePetBody <- WebUtility.HtmlDecode "{ + "photoUrls" : [ "photoUrls", "photoUrls" ], + "name" : "doggie", + "id" : 0, + "category" : { + "name" : "name", + "id" : 6 + }, + "tags" : [ { + "name" : "name", + "id" : 1 + }, { + "name" : "name", + "id" : 1 + } ], + "status" : "available" +}" + UpdatePetExamples <- UpdatePetExamples.Add("application/json", UpdatePetBody) + + UpdatePetBody <- WebUtility.HtmlDecode "<Pet> + <id>123456789</id> + <name>doggie</name> + <photoUrls> + <photoUrls>aeiou</photoUrls> + </photoUrls> + <tags> + </tags> + <status>aeiou</status> +</Pet>" + 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 diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/StoreApiTests.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/StoreApiTests.fs new file mode 100644 index 000000000000..744309736bb9 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/StoreApiTests.fs @@ -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 + // --------------------------------- + + [] + 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(400)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(404)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(200)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(200)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(400)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(404)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(200)) + |> readText + |> shouldEqual "TESTME" + } + + [] + 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(400)) + |> readText + |> shouldEqual "TESTME" + } + diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/StoreApiTestsHelper.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/StoreApiTestsHelper.fs new file mode 100644 index 000000000000..be130a9cf426 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/StoreApiTestsHelper.fs @@ -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 "{ + "petId" : 6, + "quantity" : 1, + "id" : 0, + "shipDate" : "2000-01-23T04:56:07.000+00:00", + "complete" : false, + "status" : "placed" +}" + PlaceOrderExamples <- PlaceOrderExamples.Add("application/json", PlaceOrderBody) + + let getPlaceOrderExample mediaType = + PlaceOrderExamples.[mediaType] + |> getConverter mediaType diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/TestHelper.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/TestHelper.fs new file mode 100644 index 000000000000..3b5be53c2eb1 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/TestHelper.fs @@ -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 OpenAPI.App.configureApp) + .ConfigureServices(Action 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) \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/UserApiTests.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/UserApiTests.fs new file mode 100644 index 000000000000..829336af8354 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/UserApiTests.fs @@ -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 + // --------------------------------- + + [] + 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(0)) + |> readText + |> shouldEqual "TESTME" + } + + [] + 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(0)) + |> readText + |> shouldEqual "TESTME" + } + + [] + 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(0)) + |> readText + |> shouldEqual "TESTME" + } + + [] + 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(400)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(404)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(200)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(400)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(404)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(200)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(400)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(0)) + |> readText + |> shouldEqual "TESTME" + |> ignore + } + + [] + 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(400)) + |> readText + |> shouldEqual "TESTME" + } + + [] + 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(404)) + |> readText + |> shouldEqual "TESTME" + } + diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/UserApiTestsHelper.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/UserApiTestsHelper.fs new file mode 100644 index 000000000000..352c553e0c02 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI.Tests/UserApiTestsHelper.fs @@ -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 "{ + "firstName" : "firstName", + "lastName" : "lastName", + "password" : "password", + "userStatus" : 6, + "phone" : "phone", + "id" : 0, + "email" : "email", + "username" : "username" +}" + 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 "{ + "firstName" : "firstName", + "lastName" : "lastName", + "password" : "password", + "userStatus" : 6, + "phone" : "phone", + "id" : 0, + "email" : "email", + "username" : "username" +}" + UpdateUserExamples <- UpdateUserExamples.Add("application/json", UpdateUserBody) + + let getUpdateUserExample mediaType = + UpdateUserExamples.[mediaType] + |> getConverter mediaType diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/.gitignore b/samples/server/petstore/fsharp-giraffe/OpenAPI/.gitignore new file mode 100644 index 000000000000..8d6278d820fe --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/.gitignore @@ -0,0 +1,5 @@ +**/node_modules +**/bin/ +**/obj/ +**/dist/ +**/web.config \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/README.md b/samples/server/petstore/fsharp-giraffe/OpenAPI/README.md new file mode 100644 index 000000000000..42731ee69c65 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/README.md @@ -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. + +## 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. \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/build.bat b/samples/server/petstore/fsharp-giraffe/OpenAPI/build.bat new file mode 100644 index 000000000000..8430438ab666 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/build.bat @@ -0,0 +1,3 @@ +dotnet restore src/OpenAPI.fsproj +dotnet build src/OpenAPI.fsproj + diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/build.sh b/samples/server/petstore/fsharp-giraffe/OpenAPI/build.sh new file mode 100644 index 000000000000..c5c866dafda7 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/build.sh @@ -0,0 +1,4 @@ +#!/bin/sh +dotnet restore src/OpenAPI.fsproj +dotnet build src/OpenAPI.fsproj + diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/OpenAPI.fsproj b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/OpenAPI.fsproj new file mode 100644 index 000000000000..a5d1a1bebb10 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/OpenAPI.fsproj @@ -0,0 +1,49 @@ + + + OpenAPI + OpenAPI + netcoreapp2.2 + portable + false + $(MSBuildThisFileDirectory) + OpenAPI + OpenAPI + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/Program.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/Program.fs new file mode 100644 index 000000000000..59c998288534 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/Program.fs @@ -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 "/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 "/v2/pet/{petId}" (fun x -> challenge ApiKeyDefaults.AuthenticationScheme >=> requiresAuthentication authFailure >=> PetApiHandler.GetPetById x); + HttpPut >=> route "/v2/pet" >=> requiresAuthentication authFailure >=> PetApiHandler.UpdatePet; + HttpPost >=> routeBind "/v2/pet/{petId}" (fun x -> requiresAuthentication authFailure >=> PetApiHandler.UpdatePetWithForm x); + HttpPost >=> routeBind "/v2/pet/{petId}/uploadImage" (fun x -> requiresAuthentication authFailure >=> PetApiHandler.UploadFile x); + HttpDelete >=> routeBind "/v2/store/order/{orderId}" (fun x -> StoreApiHandler.DeleteOrder x); + HttpGet >=> route "/v2/store/inventory" >=> challenge ApiKeyDefaults.AuthenticationScheme >=> requiresAuthentication authFailure >=> StoreApiHandler.GetInventory; + HttpGet >=> routeBind "/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 "/v2/user/{username}" (fun x -> challenge ApiKeyDefaults.AuthenticationScheme >=> requiresAuthentication authFailure >=> UserApiHandler.DeleteUser x); + HttpGet >=> routeBind "/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 "/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 + + [] + let main _ = + let builder = WebHost.CreateDefaultBuilder() + .Configure(Action configureApp) + .ConfigureServices(configureServices) + .ConfigureLogging(configureLogging) + |> CustomHandlers.configureWebHost + builder.Build() + .Run() + 0 \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/PetApiHandler.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/PetApiHandler.fs new file mode 100644 index 000000000000..634acdf18752 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/PetApiHandler.fs @@ -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 = + + /// + /// + /// + + //#region AddPet + /// + /// Add a new pet to the store + /// + + let AddPet : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + task { + let! bodyParams = + ctx.BindJsonAsync() + 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 + /// + /// Deletes a pet + /// + + 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 + /// + /// Finds Pets by status + /// + + let FindPetsByStatus : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + task { + let queryParams = ctx.TryBindQueryString() + 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 + /// + /// Finds Pets by tags + /// + + let FindPetsByTags : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + task { + let queryParams = ctx.TryBindQueryString() + 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 + /// + /// Find pet by ID + /// + + 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 + /// + /// Update an existing pet + /// + + let UpdatePet : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + task { + let! bodyParams = + ctx.BindJsonAsync() + 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 + /// + /// Updates a pet in the store with form data + /// + + let UpdatePetWithForm (pathParams:UpdatePetWithFormPathParams) : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + task { + let! formParams = ctx.TryBindFormAsync() + 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 + /// + /// uploads an image + /// + + let UploadFile (pathParams:UploadFilePathParams) : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + task { + let! formParams = ctx.TryBindFormAsync() + 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 + + + diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/PetApiHandlerParams.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/PetApiHandlerParams.fs new file mode 100644 index 000000000000..6bc290e74a7a --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/PetApiHandlerParams.fs @@ -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 + [] + type AddPetBodyParams = Pet + //#endregion + + + type AddPetStatusCode405Response = { + content:string; + + } + type AddPetResult = AddPetStatusCode405 of AddPetStatusCode405Response + + type AddPetArgs = { + bodyParams:AddPetBodyParams + } + //#region Path parameters + [] + type DeletePetPathParams = { + petId : int64 ; + } + //#endregion + + //#region Header parameters + [] + type DeletePetHeaderParams = { + apiKey : string option; + } + //#endregion + + + type DeletePetStatusCode400Response = { + content:string; + + } + type DeletePetResult = DeletePetStatusCode400 of DeletePetStatusCode400Response + + type DeletePetArgs = { + headerParams:DeletePetHeaderParams; + pathParams:DeletePetPathParams; + } + + //#region Query parameters + [] + 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; + } + + //#region Query parameters + [] + 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; + } + //#region Path parameters + [] + 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 + [] + 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 + [] + type UpdatePetWithFormPathParams = { + petId : int64 ; + } + //#endregion + + //#region Form parameters + [] + 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 + } + //#region Path parameters + [] + type UploadFilePathParams = { + petId : int64 ; + } + //#endregion + + //#region Form parameters + [] + 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 + } + \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/PetApiServiceInterface.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/PetApiServiceInterface.fs new file mode 100644 index 000000000000..54cffde3f3c9 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/PetApiServiceInterface.fs @@ -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 \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/StoreApiHandler.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/StoreApiHandler.fs new file mode 100644 index 000000000000..91592d5af51f --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/StoreApiHandler.fs @@ -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 = + + /// + /// + /// + + //#region DeleteOrder + /// + /// Delete purchase order by ID + /// + + 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 + /// + /// Returns pet inventories by status + /// + + 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 + /// + /// Find purchase order by ID + /// + + 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 + /// + /// Place an order for a pet + /// + + let PlaceOrder : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + task { + let! bodyParams = + ctx.BindJsonAsync() + 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 + + + diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/StoreApiHandlerParams.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/StoreApiHandlerParams.fs new file mode 100644 index 000000000000..2c9226f8abfe --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/StoreApiHandlerParams.fs @@ -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 + [] + 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; + + } + type GetInventoryResult = GetInventoryDefaultStatusCode of GetInventoryDefaultStatusCodeResponse + + //#region Path parameters + [] + 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 + [] + type PlaceOrderBodyParams = Order + //#endregion + + + type PlaceOrderDefaultStatusCodeResponse = { + content:Order; + + } + + type PlaceOrderStatusCode400Response = { + content:string; + + } + type PlaceOrderResult = PlaceOrderDefaultStatusCode of PlaceOrderDefaultStatusCodeResponse|PlaceOrderStatusCode400 of PlaceOrderStatusCode400Response + + type PlaceOrderArgs = { + bodyParams:PlaceOrderBodyParams + } + \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/StoreApiServiceInterface.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/StoreApiServiceInterface.fs new file mode 100644 index 000000000000..d949973eb07a --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/StoreApiServiceInterface.fs @@ -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 \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/UserApiHandler.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/UserApiHandler.fs new file mode 100644 index 000000000000..6994e2397416 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/UserApiHandler.fs @@ -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 = + + /// + /// + /// + + //#region CreateUser + /// + /// Create user + /// + + let CreateUser : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + task { + let! bodyParams = + ctx.BindJsonAsync() + 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 + /// + /// Creates list of users with given input array + /// + + let CreateUsersWithArrayInput : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + task { + let! bodyParams = + ctx.BindJsonAsync() + 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 + /// + /// Creates list of users with given input array + /// + + let CreateUsersWithListInput : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + task { + let! bodyParams = + ctx.BindJsonAsync() + 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 + /// + /// Delete user + /// + + 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 + /// + /// Get user by user name + /// + + 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 + /// + /// Logs user into the system + /// + + let LoginUser : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + task { + let queryParams = ctx.TryBindQueryString() + 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 + /// + /// Logs out current logged in user session + /// + + 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 + /// + /// Updated user + /// + + let UpdateUser (pathParams:UpdateUserPathParams) : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + task { + let! bodyParams = + ctx.BindJsonAsync() + 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 + + + diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/UserApiHandlerParams.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/UserApiHandlerParams.fs new file mode 100644 index 000000000000..42c51c93a664 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/UserApiHandlerParams.fs @@ -0,0 +1,169 @@ +namespace OpenAPI + +open OpenAPI.Model.User +open System.Collections.Generic +open System + +module UserApiHandlerParams = + + + //#region Body parameters + [] + type CreateUserBodyParams = User + //#endregion + + + type CreateUserDefaultStatusCodeResponse = { + content:string; + + } + type CreateUserResult = CreateUserDefaultStatusCode of CreateUserDefaultStatusCodeResponse + + type CreateUserArgs = { + bodyParams:CreateUserBodyParams + } + + //#region Body parameters + [] + type CreateUsersWithArrayInputBodyParams = User[] + //#endregion + + + type CreateUsersWithArrayInputDefaultStatusCodeResponse = { + content:string; + + } + type CreateUsersWithArrayInputResult = CreateUsersWithArrayInputDefaultStatusCode of CreateUsersWithArrayInputDefaultStatusCodeResponse + + type CreateUsersWithArrayInputArgs = { + bodyParams:CreateUsersWithArrayInputBodyParams + } + + //#region Body parameters + [] + type CreateUsersWithListInputBodyParams = User[] + //#endregion + + + type CreateUsersWithListInputDefaultStatusCodeResponse = { + content:string; + + } + type CreateUsersWithListInputResult = CreateUsersWithListInputDefaultStatusCode of CreateUsersWithListInputDefaultStatusCodeResponse + + type CreateUsersWithListInputArgs = { + bodyParams:CreateUsersWithListInputBodyParams + } + //#region Path parameters + [] + 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 + [] + 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 + [] + 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; + } + + + type LogoutUserDefaultStatusCodeResponse = { + content:string; + + } + type LogoutUserResult = LogoutUserDefaultStatusCode of LogoutUserDefaultStatusCodeResponse + + //#region Path parameters + [] + type UpdateUserPathParams = { + username : string ; + } + //#endregion + + //#region Body parameters + [] + 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 + } + \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/UserApiServiceInterface.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/UserApiServiceInterface.fs new file mode 100644 index 000000000000..1fe15fe1c6c6 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/api/UserApiServiceInterface.fs @@ -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 \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/auth/AuthSchemes.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/auth/AuthSchemes.fs new file mode 100644 index 000000000000..5e1bc765d48f --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/auth/AuthSchemes.fs @@ -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() + 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 + + \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/helpers/Helpers.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/helpers/Helpers.fs new file mode 100644 index 000000000000..6dcbe4ad94c0 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/helpers/Helpers.fs @@ -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 \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/impl/CustomHandlers.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/impl/CustomHandlers.fs new file mode 100644 index 000000000000..ef85330bb0e8 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/impl/CustomHandlers.fs @@ -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(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) = + () diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/impl/PetApiService.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/impl/PetApiService.fs new file mode 100644 index 000000000000..948ee5ce71a2 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/impl/PetApiService.fs @@ -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 \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/impl/StoreApiService.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/impl/StoreApiService.fs new file mode 100644 index 000000000000..ccb52b6e6e2a --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/impl/StoreApiService.fs @@ -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 // 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 \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/impl/UserApiService.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/impl/UserApiService.fs new file mode 100644 index 000000000000..853d8a3d3aef --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/impl/UserApiService.fs @@ -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 \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/ApiResponse.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/ApiResponse.fs new file mode 100644 index 000000000000..d9e30631e481 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/ApiResponse.fs @@ -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 + \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/Category.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/Category.fs new file mode 100644 index 000000000000..0e3de7ff3b4c --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/Category.fs @@ -0,0 +1,16 @@ +namespace OpenAPI.Model + +open System +open System.Collections.Generic + +module Category = + + //#region Category + + + type Category = { + Id : int64; + Name : string; + } + //#endregion + \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/InlineObject.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/InlineObject.fs new file mode 100644 index 000000000000..6af33f3dbdda --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/InlineObject.fs @@ -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 + \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/InlineObject1.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/InlineObject1.fs new file mode 100644 index 000000000000..b3c704a10a76 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/InlineObject1.fs @@ -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 + \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/Order.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/Order.fs new file mode 100644 index 000000000000..5718bf1b63c2 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/Order.fs @@ -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; + Status : StatusEnum; + Complete : bool; + } + //#endregion + \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/Pet.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/Pet.fs new file mode 100644 index 000000000000..720d699dce2b --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/Pet.fs @@ -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 + \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/Tag.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/Tag.fs new file mode 100644 index 000000000000..78814a494300 --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/Tag.fs @@ -0,0 +1,16 @@ +namespace OpenAPI.Model + +open System +open System.Collections.Generic + +module Tag = + + //#region Tag + + + type Tag = { + Id : int64; + Name : string; + } + //#endregion + \ No newline at end of file diff --git a/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/User.fs b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/User.fs new file mode 100644 index 000000000000..1b80bea20dae --- /dev/null +++ b/samples/server/petstore/fsharp-giraffe/OpenAPI/src/model/User.fs @@ -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 + \ No newline at end of file