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