[swift] New 'swift-combine' client generator for swift (#15823)

* swift-alt-gen init

* swift-alt-gen in progress

* swift-alt added runtime

* swift-alt added transport

* swift-alt added example

* swift-alt v0.1.0

* swift-alt implemented form encoded body

* swift-alt fixed array of enums to string

* swift-alt v0.2.0

* swift-alt v0.3.0

* swift-alt v0.4.0

* swift-alt v0.5.0

* swift-alt v0.6.0

* swift-alt v0.7.0

* swift-alt v0.8.0

* swift-alt v0.9.0

* swift-alt v0.12.0

* swift-alt v0.13.0

* swift-alt v0.14.0

* swift-alt v0.15.0

* swift-alt v0.16.0

* swift-alt v0.17.0

* swift-alt v0.18.0

* swift-alt v0.19.0 Support for raw value in header

* swift-alt v0.20.0

* swift-alt v0.21.0

* swift-alt v0.22.0

* swift-combine v0.23.0

* swift-combine PR rules adoption

* swift-combine: updated transport

* Updated bitrise.yml file

* Fixed bitrise pipeline for swift-combine

* Fixed code review comment
This commit is contained in:
Anton Davydov 2023-06-22 04:25:32 +03:00 committed by GitHub
parent d160b827de
commit 9f3d9a5e8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 4098 additions and 0 deletions

View File

@ -0,0 +1,7 @@
generatorName: swift-combine
outputDir: samples/client/petstore/swift-combine/client
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml
templateDir: modules/openapi-generator/src/main/resources/swift-combine
additionalProperties:
hideGenerationTimestamp: "true"
projectName: "PetstoreOpenAPI"

View File

@ -27,6 +27,15 @@ workflows:
set -e set -e
./samples/client/petstore/swift5/swift5_test_all.sh ./samples/client/petstore/swift5/swift5_test_all.sh
- script@1.2.0:
title: Run swift-combine tests
inputs:
- content: |
#!/usr/bin/env bash
set -e
./samples/client/petstore/swift-combine/swift-combine_test_all.sh
meta: meta:
bitrise.io: bitrise.io:

View File

@ -59,6 +59,7 @@ The following generators are available:
* [scala-gatling](generators/scala-gatling.md) * [scala-gatling](generators/scala-gatling.md)
* [scala-sttp](generators/scala-sttp.md) * [scala-sttp](generators/scala-sttp.md)
* [scalaz](generators/scalaz.md) * [scalaz](generators/scalaz.md)
* [swift-combine](generators/swift-combine.md)
* [swift5](generators/swift5.md) * [swift5](generators/swift5.md)
* [typescript (experimental)](generators/typescript.md) * [typescript (experimental)](generators/typescript.md)
* [typescript-angular](generators/typescript-angular.md) * [typescript-angular](generators/typescript-angular.md)

View File

@ -0,0 +1,328 @@
---
title: Documentation for the swift-combine Generator
---
## METADATA
| Property | Value | Notes |
| -------- | ----- | ----- |
| generator name | swift-combine | pass this to the generate command after -g |
| generator stability | STABLE | |
| generator type | CLIENT | |
| generator language | Swift | |
| generator default templating engine | mustache | |
| helpTxt | Generates a Swift Combine client library. | |
## CONFIG OPTIONS
These options may be applied as additional-properties (cli) or configOptions (plugins). Refer to [configuration docs](https://openapi-generator.tech/docs/configuration) for more details.
| Option | Description | Values | Default |
| ------ | ----------- | ------ | ------- |
|allowUnicodeIdentifiers|boolean, toggles whether unicode identifiers are allowed in names or not, default is false| |false|
|apiNamePrefix|Prefix that will be appended to all API names ('tags'). Default: empty string. e.g. Pet => Pet.| |null|
|disallowAdditionalPropertiesIfNotPresent|If false, the 'additionalProperties' implementation (set to true by default) is compliant with the OAS and JSON schema specifications. If true (default), keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.|<dl><dt>**false**</dt><dd>The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications.</dd><dt>**true**</dt><dd>Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.</dd></dl>|true|
|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true|
|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.|<dl><dt>**false**</dt><dd>No changes to the enum's are made, this is the default option.</dd><dt>**true**</dt><dd>With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the enum case sent by the server is not known by the client/spec, can safely be decoded to this case.</dd></dl>|false|
|legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default).|<dl><dt>**true**</dt><dd>The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.</dd><dt>**false**</dt><dd>The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.</dd></dl>|true|
|mapFileBinaryToData|Map File and Binary to Data (default: true)| |true|
|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false|
|projectName|Project name in Xcode| |null|
|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true|
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
## IMPORT MAPPING
| Type/Alias | Imports |
| ---------- | ------- |
## INSTANTIATION TYPES
| Type/Alias | Instantiated By |
| ---------- | --------------- |
|array|Array|
|list|Array|
## LANGUAGE PRIMITIVES
<ul class="column-ul">
<li>Any</li>
<li>AnyObject</li>
<li>Bool</li>
<li>Character</li>
<li>Data</li>
<li>Date</li>
<li>Decimal</li>
<li>Double</li>
<li>Float</li>
<li>Int</li>
<li>Int32</li>
<li>Int64</li>
<li>String</li>
<li>URL</li>
<li>UUID</li>
<li>Void</li>
<li>[String: Any]</li>
</ul>
## RESERVED WORDS
<ul class="column-ul">
<li>#available</li>
<li>#colorLiteral</li>
<li>#column</li>
<li>#else</li>
<li>#elseif</li>
<li>#endif</li>
<li>#file</li>
<li>#fileLiteral</li>
<li>#function</li>
<li>#if</li>
<li>#imageLiteral</li>
<li>#line</li>
<li>#selector</li>
<li>#sourceLocation</li>
<li>Any</li>
<li>AnyObject</li>
<li>Array</li>
<li>Bool</li>
<li>COLUMN</li>
<li>Character</li>
<li>Class</li>
<li>ClosedRange</li>
<li>Codable</li>
<li>CountableClosedRange</li>
<li>CountableRange</li>
<li>Data</li>
<li>Decodable</li>
<li>Dictionary</li>
<li>Double</li>
<li>Encodable</li>
<li>Error</li>
<li>FILE</li>
<li>FUNCTION</li>
<li>Float</li>
<li>Float32</li>
<li>Float64</li>
<li>Float80</li>
<li>Int</li>
<li>Int16</li>
<li>Int32</li>
<li>Int64</li>
<li>Int8</li>
<li>LINE</li>
<li>OptionSet</li>
<li>Optional</li>
<li>Protocol</li>
<li>Range</li>
<li>Result</li>
<li>Self</li>
<li>Set</li>
<li>StaticString</li>
<li>String</li>
<li>Type</li>
<li>UInt</li>
<li>UInt16</li>
<li>UInt32</li>
<li>UInt64</li>
<li>UInt8</li>
<li>URL</li>
<li>Unicode</li>
<li>Void</li>
<li>_</li>
<li>as</li>
<li>associatedtype</li>
<li>associativity</li>
<li>break</li>
<li>case</li>
<li>catch</li>
<li>class</li>
<li>continue</li>
<li>convenience</li>
<li>default</li>
<li>defer</li>
<li>deinit</li>
<li>didSet</li>
<li>do</li>
<li>dynamic</li>
<li>dynamicType</li>
<li>else</li>
<li>enum</li>
<li>extension</li>
<li>fallthrough</li>
<li>false</li>
<li>fileprivate</li>
<li>final</li>
<li>for</li>
<li>func</li>
<li>get</li>
<li>guard</li>
<li>if</li>
<li>import</li>
<li>in</li>
<li>indirect</li>
<li>infix</li>
<li>init</li>
<li>inout</li>
<li>internal</li>
<li>is</li>
<li>lazy</li>
<li>left</li>
<li>let</li>
<li>mutating</li>
<li>nil</li>
<li>none</li>
<li>nonmutating</li>
<li>open</li>
<li>operator</li>
<li>optional</li>
<li>override</li>
<li>postfix</li>
<li>precedence</li>
<li>prefix</li>
<li>private</li>
<li>protocol</li>
<li>public</li>
<li>repeat</li>
<li>required</li>
<li>rethrows</li>
<li>return</li>
<li>right</li>
<li>self</li>
<li>set</li>
<li>static</li>
<li>struct</li>
<li>subscript</li>
<li>super</li>
<li>switch</li>
<li>throw</li>
<li>throws</li>
<li>true</li>
<li>try</li>
<li>typealias</li>
<li>unowned</li>
<li>var</li>
<li>weak</li>
<li>where</li>
<li>while</li>
<li>willSet</li>
</ul>
## FEATURE SET
### Client Modification Feature
| Name | Supported | Defined By |
| ---- | --------- | ---------- |
|BasePath|✗|ToolingExtension
|Authorizations|✗|ToolingExtension
|UserAgent|✗|ToolingExtension
|MockServer|✗|ToolingExtension
### Data Type Feature
| Name | Supported | Defined By |
| ---- | --------- | ---------- |
|Custom|✗|OAS2,OAS3
|Int32|✓|OAS2,OAS3
|Int64|✓|OAS2,OAS3
|Float|✓|OAS2,OAS3
|Double|✓|OAS2,OAS3
|Decimal|✓|ToolingExtension
|String|✓|OAS2,OAS3
|Byte|✓|OAS2,OAS3
|Binary|✓|OAS2,OAS3
|Boolean|✓|OAS2,OAS3
|Date|✓|OAS2,OAS3
|DateTime|✓|OAS2,OAS3
|Password|✓|OAS2,OAS3
|File|✓|OAS2
|Uuid|✗|
|Array|✓|OAS2,OAS3
|Null|✗|OAS3
|AnyType|✗|OAS2,OAS3
|Object|✓|OAS2,OAS3
|Maps|✓|ToolingExtension
|CollectionFormat|✓|OAS2
|CollectionFormatMulti|✓|OAS2
|Enum|✓|OAS2,OAS3
|ArrayOfEnum|✓|ToolingExtension
|ArrayOfModel|✓|ToolingExtension
|ArrayOfCollectionOfPrimitives|✓|ToolingExtension
|ArrayOfCollectionOfModel|✓|ToolingExtension
|ArrayOfCollectionOfEnum|✓|ToolingExtension
|MapOfEnum|✓|ToolingExtension
|MapOfModel|✓|ToolingExtension
|MapOfCollectionOfPrimitives|✓|ToolingExtension
|MapOfCollectionOfModel|✓|ToolingExtension
|MapOfCollectionOfEnum|✓|ToolingExtension
### Documentation Feature
| Name | Supported | Defined By |
| ---- | --------- | ---------- |
|Readme|✗|ToolingExtension
|Model|✓|ToolingExtension
|Api|✓|ToolingExtension
### Global Feature
| Name | Supported | Defined By |
| ---- | --------- | ---------- |
|Host|✓|OAS2,OAS3
|BasePath|✓|OAS2,OAS3
|Info|✓|OAS2,OAS3
|Schemes|✗|OAS2,OAS3
|PartialSchemes|✓|OAS2,OAS3
|Consumes|✓|OAS2
|Produces|✓|OAS2
|ExternalDocumentation|✓|OAS2,OAS3
|Examples|✓|OAS2,OAS3
|XMLStructureDefinitions|✗|OAS2,OAS3
|MultiServer|✗|OAS3
|ParameterizedServer|✗|OAS3
|ParameterStyling|✗|OAS3
|Callbacks|✓|OAS3
|LinkObjects|✗|OAS3
### Parameter Feature
| Name | Supported | Defined By |
| ---- | --------- | ---------- |
|Path|✓|OAS2,OAS3
|Query|✓|OAS2,OAS3
|Header|✓|OAS2,OAS3
|Body|✓|OAS2
|FormUnencoded|✓|OAS2
|FormMultipart|✓|OAS2
|Cookie|✓|OAS3
### Schema Support Feature
| Name | Supported | Defined By |
| ---- | --------- | ---------- |
|Simple|✓|OAS2,OAS3
|Composite|✓|OAS2,OAS3
|Polymorphism|✓|OAS2,OAS3
|Union|✗|OAS3
|allOf|✗|OAS2,OAS3
|anyOf|✗|OAS3
|oneOf|✗|OAS3
|not|✗|OAS3
### Security Feature
| Name | Supported | Defined By |
| ---- | --------- | ---------- |
|BasicAuth|✓|OAS2,OAS3
|ApiKey|✓|OAS2,OAS3
|OpenIDConnect|✗|OAS3
|BearerToken|✓|OAS3
|OAuth2_Implicit|✓|OAS2,OAS3
|OAuth2_Password|✓|OAS2,OAS3
|OAuth2_ClientCredentials|✓|OAS2,OAS3
|OAuth2_AuthorizationCode|✓|OAS2,OAS3
|SignatureAuth|✗|OAS3
### Wire Format Feature
| Name | Supported | Defined By |
| ---- | --------- | ---------- |
|JSON|✓|OAS2,OAS3
|XML|✓|OAS2,OAS3
|PROTOBUF|✗|ToolingExtension
|Custom|✗|OAS2,OAS3

View File

@ -0,0 +1,817 @@
/*
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
* Copyright 2018 SmartBear Software
*
* 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
*
* https://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 io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.ComposedSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.WordUtils;
import org.openapitools.codegen.*;
import org.openapitools.codegen.meta.GeneratorMetadata;
import org.openapitools.codegen.meta.Stability;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.ModelsMap;
import org.openapitools.codegen.model.OperationsMap;
import org.openapitools.codegen.utils.ModelUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.time.OffsetDateTime;
import java.time.Instant;
import java.time.temporal.ChronoField;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
import static org.openapitools.codegen.utils.StringUtils.camelize;
public class SwiftCombineClientCodegen extends DefaultCodegen implements CodegenConfig {
private final Logger LOGGER = LoggerFactory.getLogger(SwiftCombineClientCodegen.class);
public static final String PROJECT_NAME = "projectName";
public static final String MAP_FILE_BINARY_TO_DATA = "mapFileBinaryToData";
protected String projectName = "OpenAPIClient";
protected String privateFolder = "Sources/Private";
protected String sourceFolder = "Sources";
protected String transportFolder = "OpenAPITransport";
protected List<String> notCodableTypes = Arrays.asList("Any", "AnyObject", "[String: Any]", "[String: [String: Any]]", "[Any]");
protected boolean mapFileBinaryToData = true;
protected boolean anyDecoderWasAdded = false;
/**
* Constructor for the swift language codegen module.
*/
public SwiftCombineClientCodegen() {
super();
this.supportsMultipleInheritance = true;
this.useOneOfInterfaces = true;
this.supportsAdditionalPropertiesWithComposedSchema = true;
generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata)
.stability(Stability.STABLE)
.build();
outputFolder = "generated-code" + File.separator + "swift";
modelTemplateFiles.put("model.mustache", ".swift");
apiTemplateFiles.put("api.mustache", ".swift");
embeddedTemplateDir = templateDir = "swift-combine";
apiPackage = File.separator + "APIs";
modelPackage = File.separator + "Models";
languageSpecificPrimitives = new HashSet<>(
Arrays.asList(
"Int",
"Int32",
"Int64",
"Float",
"Double",
"Bool",
"Void",
"String",
"Data",
"Date",
"Character",
"UUID",
"URL",
"AnyObject",
"Any",
"[String: Any]",
"Decimal")
);
defaultIncludes = new HashSet<>(
Arrays.asList(
"Data",
"Date",
"URL", // for file
"UUID",
"Array",
"Dictionary",
"Set",
"Any",
"Empty",
"AnyObject",
"Decimal")
);
reservedWords = new HashSet<>(
Arrays.asList(
// Swift keywords. This list is taken from here:
// https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/LexicalStructure.html#//apple_ref/doc/uid/TP40014097-CH30-ID410
//
// Keywords used in declarations
"associatedtype", "class", "deinit", "enum", "extension", "fileprivate", "func", "import", "init",
"inout", "internal", "let", "open", "operator", "private", "protocol", "public", "static", "struct",
"subscript", "typealias", "var",
// Keywords uses in statements
"break", "case", "continue", "default", "defer", "do", "else", "fallthrough", "for", "guard", "if",
"in", "repeat", "return", "switch", "where", "while",
// Keywords used in expressions and types
"as", "Any", "catch", "false", "is", "nil", "rethrows", "super", "self", "Self", "throw", "throws", "true", "try",
// Keywords used in patterns
"_",
// Keywords that begin with a number sign
"#available", "#colorLiteral", "#column", "#else", "#elseif", "#endif", "#file", "#fileLiteral", "#function", "#if",
"#imageLiteral", "#line", "#selector", "#sourceLocation",
// Keywords reserved in particular contexts
"associativity", "convenience", "dynamic", "didSet", "final", "get", "infix", "indirect", "lazy", "left",
"mutating", "none", "nonmutating", "optional", "override", "postfix", "precedence", "prefix", "Protocol",
"required", "right", "set", "Type", "unowned", "weak", "willSet",
//
// Swift Standard Library types
// https://developer.apple.com/documentation/swift
//
// Numbers and Basic Values
"Bool", "Int", "Double", "Float", "Range", "ClosedRange", "Error", "Optional",
// Special-Use Numeric Types
"UInt", "UInt8", "UInt16", "UInt32", "UInt64", "Int8", "Int16", "Int32", "Int64", "Float80", "Float32", "Float64",
// Strings and Text
"String", "Character", "Unicode", "StaticString",
// Collections
"Array", "Dictionary", "Set", "OptionSet", "CountableRange", "CountableClosedRange",
// The following are commonly-used Foundation types
"URL", "Data", "Codable", "Encodable", "Decodable", "Result",
// The following are other words we want to reserve
"Void", "AnyObject", "Class", "dynamicType", "COLUMN", "FILE", "FUNCTION", "LINE"
)
);
typeMapping = new HashMap<>();
typeMapping.put("array", "Array");
typeMapping.put("map", "Dictionary");
typeMapping.put("set", "Set");
typeMapping.put("date", "Date");
typeMapping.put("Date", "Date");
typeMapping.put("DateTime", "Date");
typeMapping.put("boolean", "Bool");
typeMapping.put("string", "String");
typeMapping.put("char", "Character");
typeMapping.put("short", "Int");
typeMapping.put("int", "Int");
typeMapping.put("long", "Int64");
typeMapping.put("integer", "Int");
typeMapping.put("Integer", "Int");
typeMapping.put("float", "Float");
typeMapping.put("number", "Double");
typeMapping.put("double", "Double");
typeMapping.put("ByteArray", "Data");
typeMapping.put("UUID", "UUID");
typeMapping.put("URI", "String");
typeMapping.put("decimal", "Decimal");
typeMapping.put("object", "[String: Any]");
typeMapping.put("AnyType", "Any");
typeMapping.put("file", "Data");
typeMapping.put("binary", "Data");
instantiationTypes.put("array", "Array");
instantiationTypes.put("list", "Array");
importMapping = new HashMap<>();
cliOptions.add(new CliOption(PROJECT_NAME, "Project name in Xcode"));
cliOptions.add(new CliOption(CodegenConstants.API_NAME_PREFIX, CodegenConstants.API_NAME_PREFIX_DESC));
cliOptions.add(new CliOption(MAP_FILE_BINARY_TO_DATA,
"Map File and Binary to Data (default: true)")
.defaultValue(Boolean.TRUE.toString()));
}
@Override
public CodegenType getTag() {
return CodegenType.CLIENT;
}
@Override
public String getName() {
return "swift-combine";
}
@Override
public String getHelp() {
return "Generates a Swift Combine client library.";
}
@Override
public void processOpts() {
super.processOpts();
anyDecoderWasAdded = false;
if (StringUtils.isEmpty(System.getenv("SWIFT_POST_PROCESS_FILE"))) {
LOGGER.info("Environment variable SWIFT_POST_PROCESS_FILE not defined so the Swift code may not be properly formatted. To define it, try 'export SWIFT_POST_PROCESS_FILE=/usr/local/bin/swiftformat' (Linux/Mac)");
LOGGER.info("NOTE: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI).");
}
// Setup project name
if (additionalProperties.containsKey(PROJECT_NAME)) {
setProjectName((String) additionalProperties.get(PROJECT_NAME));
} else {
additionalProperties.put(PROJECT_NAME, projectName);
}
supportingFiles.add(new SupportingFile("Package.mustache",
projectName,
"Package.swift"));
supportingFiles.add(new SupportingFile("OpenAPITransportPackage.mustache",
transportFolder,
"Package.swift"));
supportingFiles.add(new SupportingFile("OpenAPITransport.mustache",
transportFolder + File.separator + sourceFolder,
"OpenAPITransport.swift"));
supportingFiles.add(new SupportingFile("OpenISO8601DateFormatter.mustache",
projectName + File.separator + privateFolder,
"OpenISO8601DateFormatter.swift"));
if (additionalProperties.containsKey(MAP_FILE_BINARY_TO_DATA)) {
mapFileBinaryToData = convertPropertyToBooleanAndWriteBack(MAP_FILE_BINARY_TO_DATA);
}
additionalProperties.put(MAP_FILE_BINARY_TO_DATA, mapFileBinaryToData);
if (mapFileBinaryToData) {
typeMapping.put("file", "Data");
typeMapping.put("binary", "Data");
} else {
typeMapping.put("file", "URL");
typeMapping.put("binary", "URL");
}
}
@Override
protected boolean isReservedWord(String word) {
return word != null && reservedWords.contains(word); //don't lowercase as super does
}
@Override
public String escapeReservedWord(String name) {
if (this.reservedWordsMappings().containsKey(name)) {
return this.reservedWordsMappings().get(name);
}
return "_" + name;
}
@Override
public String modelFileFolder() {
return outputFolder + File.separator + projectName + File.separator + sourceFolder
+ modelPackage().replace('.', File.separatorChar);
}
@Override
public String apiFileFolder() {
return outputFolder + File.separator + projectName + File.separator + sourceFolder
+ apiPackage().replace('.', File.separatorChar);
}
@Override
public String getTypeDeclaration(Schema p) {
Schema<?> schema = ModelUtils.unaliasSchema(this.openAPI, p, importMapping);
Schema<?> target = ModelUtils.isGenerateAliasAsModel() ? p : schema;
if (ModelUtils.isArraySchema(target)) {
Schema<?> items = getSchemaItems((ArraySchema) schema);
return ModelUtils.isSet(target) && ModelUtils.isObjectSchema(items) ? "Set<" + getTypeDeclaration(items) + ">" : "[" + getTypeDeclaration(items) + "]";
} else if (ModelUtils.isMapSchema(target)) {
// Note: ModelUtils.isMapSchema(p) returns true when p is a composed schema that also defines
// additionalproperties: true
Schema<?> inner = getAdditionalProperties(target);
if (inner == null) {
LOGGER.error("`{}` (map property) does not have a proper inner type defined. Default to type:string", p.getName());
inner = new StringSchema().description("TODO default missing map inner type to string");
p.setAdditionalProperties(inner);
}
return "[String: " + getTypeDeclaration(inner) + "]";
} else if (ModelUtils.isComposedSchema(target)) {
List<Schema> schemas = ModelUtils.getInterfaces((ComposedSchema)target);
if (schemas.size() == 1) {
return getTypeDeclaration(schemas.get(0));
} else {
super.getTypeDeclaration(target);
}
}
return super.getTypeDeclaration(target);
}
@Override
public String getSchemaType(Schema p) {
String openAPIType = super.getSchemaType(p);
String type;
if (typeMapping.containsKey(openAPIType)) {
type = typeMapping.get(openAPIType);
if (languageSpecificPrimitives.contains(type) || defaultIncludes.contains(type)) {
return type;
}
} else {
type = openAPIType;
}
return toModelName(type);
}
@Override
public boolean isDataTypeFile(String dataType) {
return "URL".equals(dataType);
}
@Override
public boolean isDataTypeBinary(final String dataType) {
return "Data".equals(dataType);
}
/**
* Output the proper model name (capitalized).
*
* @param name the name of the model
* @return capitalized model name
*/
@Override
public String toModelName(String name) {
// FIXME parameter should not be assigned. Also declare it as "final"
name = sanitizeName(name);
if (!StringUtils.isEmpty(modelNameSuffix)) { // set model suffix
name = name + "_" + modelNameSuffix;
}
if (!StringUtils.isEmpty(modelNamePrefix)) { // set model prefix
name = modelNamePrefix + "_" + name;
}
// camelize the model name
// phone_number => PhoneNumber
name = camelize(name);
// model name cannot use reserved keyword, e.g. return
if (isReservedWord(name)) {
String modelName = "Model" + name;
LOGGER.warn("{} (reserved word) cannot be used as model name. Renamed to {}", name, modelName);
return modelName;
}
// model name starts with number
if (name.matches("^\\d.*")) {
// e.g. 200Response => Model200Response (after camelize)
String modelName = "Model" + name;
LOGGER.warn("{} (model name starts with number) cannot be used as model name. Renamed to {}", name,
modelName);
return modelName;
}
return name;
}
/**
* Return the capitalized file name of the model.
*
* @param name the model name
* @return the file name of the model
*/
@Override
public String toModelFilename(String name) {
// should be the same as the model name
return toModelName(name);
}
@Override
public String toDefaultValue(Schema p) {
if (p.getEnum() != null && !p.getEnum().isEmpty()) {
if (p.getDefault() != null) {
if (ModelUtils.isStringSchema(p)) {
return "." + toEnumVarName(escapeText((String) p.getDefault()), p.getType());
} else {
return "." + toEnumVarName(escapeText(p.getDefault().toString()), p.getType());
}
}
}
if (p.getDefault() != null) {
if (ModelUtils.isIntegerSchema(p) || ModelUtils.isNumberSchema(p) || ModelUtils.isBooleanSchema(p)) {
return p.getDefault().toString();
} else if (ModelUtils.isDateTimeSchema(p)) {
// Datetime time stamps in Swift are expressed as Seconds with Microsecond precision.
// In Java, we need to be creative to get the Timestamp in Microseconds as a long.
Instant instant = ((OffsetDateTime) p.getDefault()).toInstant();
long epochMicro = TimeUnit.SECONDS.toMicros(instant.getEpochSecond()) + (instant.get(ChronoField.MICRO_OF_SECOND));
return "Date(timeIntervalSince1970: " + String.valueOf(epochMicro) + ".0 / 1_000_000)";
} else if (ModelUtils.isUUIDSchema(p)) {
return "\"" + escapeText(p.getDefault().toString()) + "\"";
} else if (ModelUtils.isStringSchema(p)) {
return "\"" + escapeText((String) p.getDefault()) + "\"";
}
// TODO: Handle more cases from `ModelUtils`, such as Date
}
return null;
}
@Override
public String toInstantiationType(Schema p) {
if (ModelUtils.isMapSchema(p)) {
return getSchemaType(getAdditionalProperties(p));
} else if (ModelUtils.isArraySchema(p)) {
ArraySchema ap = (ArraySchema) p;
String inner = getSchemaType(ap.getItems());
return ModelUtils.isSet(p) ? "Set<" + inner + ">" : "[" + inner + "]";
}
return null;
}
@Override
public String toApiName(String name) {
if (name.length() == 0) {
return "DefaultAPI";
}
return camelize(apiNamePrefix + "_" + name) + "API";
}
@Override
public String toModelDocFilename(String name) {
return toModelName(name);
}
@Override
public String toApiDocFilename(String name) {
return toApiName(name);
}
@Override
public String toOperationId(String operationId) {
operationId = camelize(sanitizeName(operationId), LOWERCASE_FIRST_LETTER);
// Throw exception if method name is empty.
// This should not happen but keep the check just in case
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)) {
String newOperationId = camelize(("call_" + operationId), LOWERCASE_FIRST_LETTER);
LOGGER.warn("{} (reserved word) cannot be used as method name. Renamed to {}", operationId, newOperationId);
return newOperationId;
}
// operationId starts with a number
if (operationId.matches("^\\d.*")) {
LOGGER.warn("{} (starting with a number) cannot be used as method name. Renamed to {}", operationId, camelize(sanitizeName("call_" + operationId), LOWERCASE_FIRST_LETTER));
operationId = camelize(sanitizeName("call_" + operationId), LOWERCASE_FIRST_LETTER);
}
return operationId;
}
@Override
public String toVarName(String name) {
// sanitize name
name = sanitizeName(name);
// if it's all upper case, do nothing
if (name.matches("^[A-Z_]*$")) {
return name;
}
// camelize the variable name
// pet_id => petId
name = camelize(name, LOWERCASE_FIRST_LETTER);
// for reserved words surround with `` or append _
if (isReservedWord(name)) {
name = escapeReservedWord(name);
}
// for words starting with number, append _
if (name.matches("^\\d.*")) {
name = "_" + 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 upper case, do nothing
if (name.matches("^[A-Z_]*$")) {
return name;
}
// camelize(lower) the variable name
// pet_id => petId
name = camelize(name, LOWERCASE_FIRST_LETTER);
// for reserved words surround with ``
if (isReservedWord(name)) {
name = escapeReservedWord(name);
}
// for words starting with number, append _
if (name.matches("^\\d.*")) {
name = "_" + name;
}
return name;
}
@Override
public CodegenModel fromModel(String name, Schema schema) {
CodegenModel m = super.fromModel(name, schema);
m.optionalVars = m.optionalVars.stream().distinct().collect(Collectors.toList());
// Update allVars/requiredVars/optionalVars with isInherited
// Each of these lists contains elements that are similar, but they are all cloned
// via CodegenModel.removeAllDuplicatedProperty and therefore need to be updated
// separately.
// First find only the parent vars via baseName matching
Map<String, CodegenProperty> allVarsMap = m.allVars.stream()
.collect(Collectors.toMap(CodegenProperty::getBaseName, Function.identity()));
allVarsMap.keySet()
.removeAll(m.vars.stream().map(CodegenProperty::getBaseName).collect(Collectors.toSet()));
// Update the allVars
allVarsMap.values().forEach(p -> p.isInherited = true);
// Update any other vars (requiredVars, optionalVars)
Stream.of(m.requiredVars, m.optionalVars)
.flatMap(List::stream)
.filter(p -> allVarsMap.containsKey(p.baseName))
.forEach(p -> p.isInherited = true);
return m;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
@Override
public String toEnumValue(String value, String datatype) {
// for string, array of string
if ("String".equals(datatype) || "[String]".equals(datatype) || "[String: String]".equals(datatype)) {
return "\"" + String.valueOf(value) + "\"";
} else {
return String.valueOf(value);
}
}
@Override
public String toEnumDefaultValue(String value, String datatype) {
return datatype + "_" + value;
}
@Override
public String toEnumVarName(String name, String datatype) {
if (name.length() == 0) {
return "empty";
}
Pattern startWithNumberPattern = Pattern.compile("^\\d+");
Matcher startWithNumberMatcher = startWithNumberPattern.matcher(name);
if (startWithNumberMatcher.find()) {
String startingNumbers = startWithNumberMatcher.group(0);
String nameWithoutStartingNumbers = name.substring(startingNumbers.length());
return "_" + startingNumbers + camelize(nameWithoutStartingNumbers, LOWERCASE_FIRST_LETTER);
}
// for symbol, e.g. $, #
if (getSymbolName(name) != null) {
return camelize(WordUtils.capitalizeFully(getSymbolName(name).toUpperCase(Locale.ROOT)), LOWERCASE_FIRST_LETTER);
}
// Camelize only when we have a structure defined below
Boolean camelized = false;
if (name.matches("[A-Z][a-z0-9]+[a-zA-Z0-9]*")) {
name = camelize(name, LOWERCASE_FIRST_LETTER);
camelized = true;
}
// Reserved Name
String nameLowercase = StringUtils.lowerCase(name);
if (isReservedWord(nameLowercase)) {
return escapeReservedWord(nameLowercase);
}
// Check for numerical conversions
if ("Int".equals(datatype) || "Int32".equals(datatype) || "Int64".equals(datatype)
|| "Float".equals(datatype) || "Double".equals(datatype)) {
String varName = "number" + camelize(name);
varName = varName.replaceAll("-", "minus");
varName = varName.replaceAll("\\+", "plus");
varName = varName.replaceAll("\\.", "dot");
return varName;
}
// If we have already camelized the word, don't progress
// any further
if (camelized) {
return name;
}
char[] separators = {'-', '_', ' ', ':', '(', ')'};
return camelize(WordUtils.capitalizeFully(StringUtils.lowerCase(name), separators)
.replaceAll("[-_ :\\(\\)]", ""),
LOWERCASE_FIRST_LETTER);
}
@Override
public String toEnumName(CodegenProperty property) {
String enumName = toModelName(property.name);
// Ensure that the enum type doesn't match a reserved word or
// the variable name doesn't match the generated enum type or the
// Swift compiler will generate an error
if (isReservedWord(property.datatypeWithEnum)
|| toVarName(property.name).equals(property.datatypeWithEnum)) {
enumName = property.datatypeWithEnum + "Enum";
}
// TODO: toModelName already does something for names starting with number,
// so this code is probably never called
if (enumName.matches("\\d.*")) { // starts with number
return "_" + enumName;
} else {
return enumName;
}
}
@Override
public ModelsMap postProcessModels(ModelsMap objs) {
ModelsMap postProcessedModelsEnum = postProcessModelsEnum(objs);
List<Object> models = (List<Object>) postProcessedModelsEnum.get("models");
for (Object _mo : models) {
Map<String, Object> mo = (Map<String, Object>) _mo;
CodegenModel cm = (CodegenModel) mo.get("model");
boolean modelHasPropertyWithEscapedName = false;
for (CodegenProperty prop : cm.allVars) {
if (!prop.name.equals(prop.baseName)) {
prop.vendorExtensions.put("x-codegen-escaped-property-name", true);
modelHasPropertyWithEscapedName = true;
}
if (notCodableTypes.contains(prop.dataType) || notCodableTypes.contains(prop.baseType)) {
prop.vendorExtensions.put("x-swift-is-not-codable", true);
}
if (modelHasPropertyWithEscapedName || notCodableTypes.contains(prop.dataType) || notCodableTypes.contains(prop.baseType)) {
cm.vendorExtensions.put("x-swift-contains-not-codable", true);
addAnyDecoderIfNeeded();
}
}
if (modelHasPropertyWithEscapedName) {
cm.vendorExtensions.put("x-codegen-has-escaped-property-names", true);
}
}
return postProcessedModelsEnum;
}
@Override
public String escapeQuotationMark(String input) {
// remove " to avoid code injection
return input.replace("\"", "");
}
@Override
public String escapeUnsafeCharacters(String input) {
return input.replace("*/", "*_/").replace("/*", "/_*");
}
@Override
public void postProcessFile(File file, String fileType) {
if (file == null) {
return;
}
String swiftPostProcessFile = System.getenv("SWIFT_POST_PROCESS_FILE");
if (StringUtils.isEmpty(swiftPostProcessFile)) {
return; // skip if SWIFT_POST_PROCESS_FILE env variable is not defined
}
// only process files with swift extension
if ("swift".equals(FilenameUtils.getExtension(file.toString()))) {
String command = swiftPostProcessFile + " " + file.toString();
try {
Process p = Runtime.getRuntime().exec(command);
int exitValue = p.waitFor();
if (exitValue != 0) {
LOGGER.error("Error running the command ({}). Exit value: {}", command, exitValue);
} else {
LOGGER.info("Successfully executed: {}", command);
}
} catch (InterruptedException | IOException e) {
LOGGER.error("Error running the command ({}). Exception: {}", command, e.getMessage());
// Restore interrupted state
Thread.currentThread().interrupt();
}
}
}
@Override
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> allModels) {
Map<String, Object> objectMap = (Map<String, Object>) objs.get("operations");
HashMap<String, CodegenModel> modelMaps = new HashMap<String, CodegenModel>();
for (Object o : allModels) {
HashMap<String, Object> h = (HashMap<String, Object>) o;
CodegenModel m = (CodegenModel) h.get("model");
modelMaps.put(m.classname, m);
}
List<CodegenOperation> operations = (List<CodegenOperation>) objectMap.get("operation");
for (CodegenOperation operation : operations) {
operation.allParams.forEach(cp -> addVendorExtensions(cp, operation, modelMaps));
operation.queryParams.forEach(cp -> addVendorExtensions(cp, operation, modelMaps));
operation.headerParams.forEach(cp -> addVendorExtensions(cp, operation, modelMaps));
operation.bodyParams.forEach(cp -> addVendorExtensions(cp, operation, modelMaps));
operation.formParams.forEach(cp -> addFormVendorExtensions(cp, operation, modelMaps));
if (notCodableTypes.contains(operation.returnType) || notCodableTypes.contains(operation.returnBaseType)) {
operation.vendorExtensions.put("x-swift-is-not-codable", true);
addAnyDecoderIfNeeded();
}
List<CodegenResponse> responses = operation.responses;
for (CodegenResponse response : responses) {
if (response.is4xx || response.is5xx) {
response.vendorExtensions.put("x-swift-has-custom-error-type", true);
response.vendorExtensions.put("x-swift-custom-error-type", WordUtils.capitalize(operation.operationId) + "Error");
operation.vendorExtensions.put("x-swift-custom-error-type", WordUtils.capitalize(operation.operationId) + "Error");
}
response.vendorExtensions.put("x-swift-is-response-code-explicit", !response.code.contains("x"));
}
}
return objs;
}
protected void addVendorExtensions(CodegenParameter cp, CodegenOperation operation, HashMap<String, CodegenModel> modelMaps) {
CodegenModel model = modelMaps.get(cp.dataType);
cp.vendorExtensions.put("x-swift-use-encoder", cp.isModel);
if (cp.isArray && cp.items != null) {
CodegenModel baseModel = modelMaps.get(cp.items.dataType);
boolean isBaseTypeEnum = cp.items.isEnum || cp.isEnum || (baseModel != null && baseModel.isEnum);
cp.vendorExtensions.put("x-swift-is-base-type-enum", isBaseTypeEnum);
boolean isBaseTypeUdid = cp.items.isUuid || cp.isUuid;
cp.vendorExtensions.put("x-swift-is-base-type-udid", isBaseTypeUdid);
boolean useEncoder = !isBaseTypeEnum && !cp.items.isString || (baseModel != null && !baseModel.isString);
cp.vendorExtensions.put("x-swift-use-encoder", useEncoder);
}
if (cp.isEnum || (model != null && model.isEnum)) {
cp.vendorExtensions.put("x-swift-is-enum-type", true);
}
if (cp.isEnum) {
String newDataType = WordUtils.capitalize(operation.operationId) + WordUtils.capitalize(cp.enumName);
cp.vendorExtensions.put("x-swift-nested-enum-type", newDataType);
if (cp.isArray) {
if (cp.uniqueItems) {
cp.dataType = "Set<" + newDataType + ">";
} else {
cp.dataType = "[" + newDataType + "]";
}
} else {
cp.baseType = cp.dataType;
cp.dataType = newDataType;
}
}
}
protected void addFormVendorExtensions(CodegenParameter cp, CodegenOperation operation, HashMap<String, CodegenModel> modelMaps) {
addVendorExtensions(cp, operation, modelMaps);
if (operation.isMultipart && cp.isArray && cp.items.isFile) {
cp.vendorExtensions.put("x-swift-enumerate-multipart", true);
}
}
@Override
public void postProcess() {
System.out.println("################################################################################");
System.out.println("# Thanks for using OpenAPI Generator. #");
System.out.println("# swift combine generator is contributed by @dydus0x14 and @ptiz. #");
System.out.println("################################################################################");
}
@Override
public GeneratorLanguage generatorLanguage() { return GeneratorLanguage.SWIFT; }
protected void addAnyDecoderIfNeeded() {
if (!anyDecoderWasAdded) {
supportingFiles.add(new SupportingFile("AnyDecodable.mustache",
projectName + File.separator + privateFolder,
"AnyDecodable.swift"));
anyDecoderWasAdded = true;
}
}
}

View File

@ -138,3 +138,4 @@ org.openapitools.codegen.languages.TypeScriptReduxQueryClientCodegen
org.openapitools.codegen.languages.TypeScriptRxjsClientCodegen org.openapitools.codegen.languages.TypeScriptRxjsClientCodegen
org.openapitools.codegen.languages.WsdlSchemaCodegen org.openapitools.codegen.languages.WsdlSchemaCodegen
org.openapitools.codegen.languages.XojoClientCodegen org.openapitools.codegen.languages.XojoClientCodegen
org.openapitools.codegen.languages.SwiftCombineClientCodegen

View File

@ -0,0 +1,117 @@
// AnyDecodable.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
//
import Foundation
extension KeyedDecodingContainer {
func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
let container = try self.nestedContainer(keyedBy: AnyCodingKeys.self, forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any>? {
if contains(key) {
return try decode(type, forKey: key)
}
return nil
}
func decode(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any> {
var container = try self.nestedUnkeyedContainer(forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any>? {
if contains(key) {
return try decode(type, forKey: key)
}
return nil
}
func decode(_ type: Dictionary<String, Dictionary<String, Any>>.Type, forKey key: K) throws -> Dictionary<String, Dictionary<String, Any>> {
let container = try self.nestedContainer(keyedBy: AnyCodingKeys.self, forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Dictionary<String, Dictionary<String, Any>>.Type, forKey key: K) throws -> Dictionary<String, Dictionary<String, Any>>? {
if contains(key) {
return try decode(type, forKey: key)
}
return nil
}
func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
var dictionary = Dictionary<String, Any>()
for key in allKeys {
if let boolValue = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = boolValue
} else if let stringValue = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = stringValue
} else if let intValue = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = intValue
} else if let doubleValue = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = doubleValue
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedDictionary
} else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedArray
}
}
return dictionary
}
func decode(_ type: Dictionary<String, Dictionary<String, Any>>.Type) throws -> Dictionary<String, Dictionary<String, Any>> {
var dictionary = Dictionary<String, Dictionary<String, Any>>()
for key in allKeys {
if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedDictionary
}
}
return dictionary
}
}
extension UnkeyedDecodingContainer {
mutating func decode(_ type: Array<Any>.Type) throws -> Array<Any> {
var array: [Any] = []
while isAtEnd == false {
if let value = try? decode(Bool.self) {
array.append(value)
} else if let value = try? decode(Double.self) {
array.append(value)
} else if let value = try? decode(String.self) {
array.append(value)
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
array.append(nestedDictionary)
} else if let nestedArray = try? decode(Array<Any>.self) {
array.append(nestedArray)
}
}
return array
}
mutating func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
let nestedContainer = try self.nestedContainer(keyedBy: AnyCodingKeys.self)
return try nestedContainer.decode(type)
}
}
private struct AnyCodingKeys: CodingKey {
var stringValue: String
init(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}

View File

@ -0,0 +1,306 @@
// OpenAPITransport.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
import Foundation
import Combine
// MARK: - OpenAPITransport
public protocol OpenAPITransport: AnyObject {
var baseURL: URL? { get }
func send(request: URLRequest) -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError>
func cancelAll()
}
public struct OpenAPITransportResponse {
public let data: Data
public let statusCode: Int
public init(data: Data, statusCode: Int) {
self.data = data
self.statusCode = statusCode
}
}
public struct OpenAPITransportError: Error, CustomStringConvertible, LocalizedError {
public let statusCode: Int
public let description: String
public let errorDescription: String?
/// It might be source network error
public let nestedError: Error?
/// Data may contain additional reason info (like json payload)
public let data: Data
public init(
statusCode: Int,
description: String? = nil,
errorDescription: String? = nil,
nestedError: Error? = nil,
data: Data = Data()
) {
self.statusCode = statusCode
self.errorDescription = errorDescription
self.nestedError = nestedError
self.data = data
if let description = description {
self.description = description
} else {
var summary = "OpenAPITransportError with status \(statusCode)"
if let nestedError = nestedError {
summary.append(contentsOf: ", \(nestedError.localizedDescription)")
}
self.description = summary
}
}
}
// MARK: - Policy
/// Policy to define whether response is successful or requires authentication
public protocol ResponsePolicy {
func defineState(for request: URLRequest, output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<ResponseState, Never>
}
public enum ResponseState {
/// Return success to client
case success
/// Return error to client
case failure
/// Repeat request
case retry
}
// MARK: - Interceptor
/// Define how to customize URL request before network call
public protocol Interceptor {
/// Customize request before performing. Add headers or encrypt body for example.
func intercept(request: URLRequest) -> AnyPublisher<URLRequest, OpenAPITransportError>
/// Customize response before handling. Decrypt body for example.
func intercept(output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<URLSession.DataTaskPublisher.Output, OpenAPITransportError>
}
// MARK: - Transport delegate
public protocol OpenAPITransportDelegate: AnyObject {
func willStart(request: URLRequest)
func didFinish(request: URLRequest, response: HTTPURLResponse?, data: Data)
func didFinish(request: URLRequest, error: Error)
}
// MARK: - Implementation
open class URLSessionOpenAPITransport: OpenAPITransport {
public struct Config {
public var baseURL: URL?
public var session: URLSession
public var processor: Interceptor
public var policy: ResponsePolicy
public weak var delegate: OpenAPITransportDelegate?
public init(
baseURL: URL? = nil,
session: URLSession = .shared,
processor: Interceptor = DefaultInterceptor(),
policy: ResponsePolicy = DefaultResponsePolicy(),
delegate: OpenAPITransportDelegate? = nil
) {
self.baseURL = baseURL
self.session = session
self.processor = processor
self.policy = policy
self.delegate = delegate
}
}
private var cancellable = Set<AnyCancellable>()
public var config: Config
public var baseURL: URL? { config.baseURL }
public init(config: Config = .init()) {
self.config = config
}
open func send(request: URLRequest) -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> {
config.processor
// Add custom headers or refresh token if needed
.intercept(request: request)
.flatMap { request -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> in
self.config.delegate?.willStart(request: request)
// Perform network call
return self.config.session.dataTaskPublisher(for: request)
.mapError {
self.config.delegate?.didFinish(request: request, error: $0)
return OpenAPITransportError(statusCode: $0.code.rawValue, description: "Network call finished fails")
}
.flatMap { output in
self.config.processor.intercept(output: output)
}
.flatMap { output -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> in
let response = output.response as? HTTPURLResponse
self.config.delegate?.didFinish(request: request, response: response, data: output.data)
return self.config.policy.defineState(for: request, output: output)
.setFailureType(to: OpenAPITransportError.self)
.flatMap { state -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> in
switch state {
case .success:
let transportResponse = OpenAPITransportResponse(data: output.data, statusCode: 200)
return Result.success(transportResponse).publisher.eraseToAnyPublisher()
case .retry:
return Fail(error: OpenAPITransportError.retryError).eraseToAnyPublisher()
case .failure:
let code = response?.statusCode ?? OpenAPITransportError.noResponseCode
let transportError = OpenAPITransportError(statusCode: code, data: output.data)
return Fail(error: transportError).eraseToAnyPublisher()
}
}.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
.retry(times: 2) { error -> Bool in
return error.statusCode == OpenAPITransportError.retryError.statusCode
}.eraseToAnyPublisher()
}
open func cancelAll() {
cancellable.removeAll()
}
}
public final class DefaultInterceptor: Interceptor {
public init() {}
public func intercept(request: URLRequest) -> AnyPublisher<URLRequest, OpenAPITransportError> {
Just(request)
.setFailureType(to: OpenAPITransportError.self)
.eraseToAnyPublisher()
}
public func intercept(output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<URLSession.DataTaskPublisher.Output, OpenAPITransportError> {
Just(output)
.setFailureType(to: OpenAPITransportError.self)
.eraseToAnyPublisher()
}
}
public final class DefaultResponsePolicy: ResponsePolicy {
public init() {}
public func defineState(for request: URLRequest, output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<ResponseState, Never> {
let state: ResponseState
switch (output.response as? HTTPURLResponse)?.statusCode {
case .some(200...299): state = .success
default: state = .failure
}
return Just(state).eraseToAnyPublisher()
}
}
/// Custom transport errors. It begins with 6.. not to conflict with HTTP codes
public extension OpenAPITransportError {
static let incorrectAuthenticationCode = 600
static func incorrectAuthenticationError(_ nestedError: Error? = nil) -> OpenAPITransportError {
OpenAPITransportError(
statusCode: OpenAPITransportError.incorrectAuthenticationCode,
description: "Impossible to add authentication headers to request",
errorDescription: NSLocalizedString(
"Impossible to add authentication headers to request",
comment: "Incorrect authentication"
),
nestedError: nestedError
)
}
static let failedAuthenticationRefreshCode = 601
static func failedAuthenticationRefreshError(_ nestedError: Error? = nil) -> OpenAPITransportError {
OpenAPITransportError(
statusCode: OpenAPITransportError.failedAuthenticationRefreshCode,
description: "Error while refreshing authentication",
errorDescription: NSLocalizedString(
"Error while refreshing authentication",
comment: "Failed authentication refresh"
),
nestedError: nestedError
)
}
static let noResponseCode = 603
static func noResponseError(_ nestedError: Error? = nil) -> OpenAPITransportError {
OpenAPITransportError(
statusCode: OpenAPITransportError.noResponseCode,
description: "There is no HTTP URL response",
errorDescription: NSLocalizedString(
"There is no HTTP URL response",
comment: "No response"
),
nestedError: nestedError
)
}
static let badURLCode = 604
static func badURLError(_ nestedError: Error? = nil) -> OpenAPITransportError {
OpenAPITransportError(
statusCode: OpenAPITransportError.badURLCode,
description: "Request URL cannot be created with given parameters",
errorDescription: NSLocalizedString(
"Request URL cannot be created with given parameters",
comment: "Bad URL"
),
nestedError: nestedError
)
}
static let invalidResponseMappingCode = 605
static func invalidResponseMappingError(data: Data) -> OpenAPITransportError {
OpenAPITransportError(
statusCode: OpenAPITransportError.invalidResponseMappingCode,
description: "Response data cannot be expected object scheme",
errorDescription: NSLocalizedString(
"Response data cannot be expected object scheme",
comment: "Invalid response mapping"
),
data: data
)
}
static let retryErrorCode = 606
static let retryError = OpenAPITransportError(statusCode: OpenAPITransportError.retryErrorCode)
}
// MARK: - Private
private extension Publishers {
struct RetryIf<P: Publisher>: Publisher {
typealias Output = P.Output
typealias Failure = P.Failure
let publisher: P
let times: Int
let condition: (P.Failure) -> Bool
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
guard times > 0 else { return publisher.receive(subscriber: subscriber) }
publisher.catch { (error: P.Failure) -> AnyPublisher<Output, Failure> in
if condition(error) {
return RetryIf(publisher: publisher, times: times - 1, condition: condition).eraseToAnyPublisher()
} else {
return Fail(error: error).eraseToAnyPublisher()
}
}.receive(subscriber: subscriber)
}
}
}
private extension Publisher {
func retry(times: Int, if condition: @escaping (Failure) -> Bool) -> Publishers.RetryIf<Self> {
Publishers.RetryIf(publisher: self, times: times, condition: condition)
}
}

View File

@ -0,0 +1,25 @@
// swift-tools-version:5.1
import PackageDescription
let package = Package(
name: "OpenAPITransport",
platforms: [
.iOS(.v13),
.macOS(.v10_15)
],
products: [
.library(
name: "OpenAPITransport",
targets: ["OpenAPITransport"]
),
],
dependencies: [],
targets: [
.target(
name: "OpenAPITransport",
dependencies: [],
path: "Sources"
),
]
)

View File

@ -0,0 +1,46 @@
//
// OpenISO8601DateFormatter.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
//
import Foundation
// https://stackoverflow.com/a/50281094/976628
class OpenISO8601DateFormatter: DateFormatter {
static let withoutSeconds: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
return formatter
}()
private func setup() {
calendar = Calendar(identifier: .iso8601)
locale = Locale(identifier: "en_US_POSIX")
timeZone = TimeZone(secondsFromGMT: 0)
dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
}
static var shared = OpenISO8601DateFormatter()
override init() {
super.init()
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override func date(from string: String) -> Date? {
if let result = super.date(from: string) {
return result
}
return OpenISO8601DateFormatter.withoutSeconds.date(from: string)
}
}

View File

@ -0,0 +1,25 @@
// swift-tools-version:5.1
import PackageDescription
let package = Package(
name: "{{projectName}}",
platforms: [
.iOS(.v13),
.macOS(.v10_15)
],
products: [
.library(
name: "{{projectName}}",
targets: ["{{projectName}}"]
),
],
dependencies: [.package(path: "../OpenAPITransport")],
targets: [
.target(
name: "{{projectName}}",
dependencies: [.byName(name: "OpenAPITransport")],
path: "Sources"
),
]
)

View File

@ -0,0 +1,215 @@
{{#operations}}//
// {{classname}}.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
import Foundation
import Combine
import OpenAPITransport
{{#description}}
/// {{{.}}} {{/description}}
open class {{classname}} {
private let transport: OpenAPITransport
public var encoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .formatted(OpenISO8601DateFormatter())
return encoder
}()
public var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(OpenISO8601DateFormatter())
return decoder
}()
public var baseURL = URL(string: "{{{basePath}}}")
public init(_ transport: OpenAPITransport) {
self.transport = transport
}
{{#operation}}
{{#allParams}}
{{#isEnum}}
///
/// Enum for parameter {{paramName}}
///
public enum {{{vendorExtensions.x-swift-nested-enum-type}}}: {{{baseType}}}, Codable, CaseIterable {
{{#allowableValues}}
{{#enumVars}}
case {{name}} = {{{value}}}
{{/enumVars}}
{{/allowableValues}}
}
{{/isEnum}}
{{/allParams}}
{{#vendorExtensions.x-swift-custom-error-type}}
public enum {{{vendorExtensions.x-swift-custom-error-type}}}: Error, CustomStringConvertible {
{{#responses}}
{{#vendorExtensions.x-swift-has-custom-error-type}}
// {{{message}}}
case code{{{code}}}Error{{#dataType}}({{{dataType}}}){{/dataType}}
{{/vendorExtensions.x-swift-has-custom-error-type}}
{{/responses}}
public var description: String {
switch self {
{{#responses}}
{{#vendorExtensions.x-swift-has-custom-error-type}}
case .code{{{code}}}Error{{#dataType}}(let object){{/dataType}}:
return "{{{vendorExtensions.x-swift-custom-error-type}}}: {{{message}}}{{#dataType}}: \(object){{/dataType}}"
{{/vendorExtensions.x-swift-has-custom-error-type}}
{{/responses}}
}
}
}
{{/vendorExtensions.x-swift-custom-error-type}}
{{#summary}}
/// {{{.}}}
{{/summary}}
/// - {{httpMethod}} {{{path}}}{{#notes}}
/// - {{{.}}}{{/notes}}{{#subresourceOperation}}
/// - subresourceOperation: {{.}}{{/subresourceOperation}}{{#defaultResponse}}
/// - defaultResponse: {{.}}{{/defaultResponse}}
{{#authMethods}}
/// - {{#isBasicBasic}}BASIC{{/isBasicBasic}}{{#isOAuth}}OAuth{{/isOAuth}}{{#isApiKey}}API Key{{/isApiKey}}:
/// - type: {{type}}{{#keyParamName}} {{keyParamName}} {{#isKeyInQuery}}(QUERY){{/isKeyInQuery}}{{#isKeyInHeaer}}(HEADER){{/isKeyInHeaer}}{{/keyParamName}}
/// - name: {{name}}
{{/authMethods}}
{{#hasResponseHeaders}}
/// - responseHeaders: [{{#responseHeaders}}{{{baseName}}}({{{dataType}}}){{^-last}}, {{/-last}}{{/responseHeaders}}]
{{/hasResponseHeaders}}
{{#externalDocs}}
/// - externalDocs:
{{#url}}
/// url: {{.}}
{{/url}}
{{#description}}
/// description: {{.}}
{{/description}}
{{/externalDocs}}
{{#allParams}}
/// - parameter {{paramName}}: ({{#isFormParam}}form{{/isFormParam}}{{#isQueryParam}}query{{/isQueryParam}}{{#isPathParam}}path{{/isPathParam}}{{#isHeaderParam}}header{{/isHeaderParam}}{{#isBodyParam}}body{{/isBodyParam}}) {{description}} {{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}
{{/allParams}}
/// - returns: AnyPublisher<{{{returnType}}}{{^returnType}}Void{{/returnType}}, Error> {{description}}
{{#isDeprecated}}
@available(*, deprecated, message: "Deprecated API operation")
{{/isDeprecated}}
open func {{operationId}}({{#allParams}}{{paramName}}: {{{dataType}}}{{^required}}? = nil{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) -> AnyPublisher<{{{returnType}}}{{^returnType}}Void{{/returnType}}, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
{{#pathParams}}{{#-last}}var{{/-last}}{{/pathParams}}{{^pathParams}}let{{/pathParams}} path = "{{path}}"
{{#pathParams}}
{{#required}}path = path.replacingOccurrences(of: "{{=<% %>=}}{<%baseName%>}<%={{ }}=%>", with: {{> toString}}){{/required}}{{^required}}if let {{paramName}} = {{paramName}} { path = path.replacingOccurrences(of: "{{=<% %>=}}{<%baseName%>}<%={{ }}=%>", with: {{> toString}}) } {{/required}}
{{/pathParams}}
let url = baseURL.appendingPathComponent(path)
{{#hasQueryParams}}var{{/hasQueryParams}}{{^hasQueryParams}}let{{/hasQueryParams}} components = URLComponents(url: url, resolvingAgainstBaseURL: false)
{{#hasQueryParams}}
var queryItems: [URLQueryItem] = []
{{#queryParams}}
{{#required}}queryItems.append(URLQueryItem(name: "{{baseName}}", value: {{> toString}})){{/required}}{{^required}}if let {{paramName}} = {{paramName}} { queryItems.append(URLQueryItem(name: "{{baseName}}", value: {{> toString}})) } {{/required}}
{{/queryParams}}
components?.queryItems = queryItems
{{/hasQueryParams}}
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "{{httpMethod}}"
{{! Begin headers }}
{{#headerParams}}
{{#-last}}
var headers = [String: String]()
{{/-last}}
{{/headerParams}}
{{#headerParams}}
{{#required}}headers["{{baseName}}"] = {{> toString}}{{/required}}{{^required}}if let {{paramName}} = {{paramName}} { headers["{{baseName}}"] = {{> toString}} }{{/required}}
{{/headerParams}}
{{#headerParams}}
{{#-last}}
request.allHTTPHeaderFields = headers
{{/-last}}
{{/headerParams}}
{{! Begin body }}
{{#hasBodyParam}}
{{#bodyParam}}
request.httpBody = try self.encoder.encode({{paramName}})
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
{{/bodyParam}}
{{/hasBodyParam}}
{{! Begin multipart form }}
{{#hasFormParams}}
{{#isMultipart}}
let multipartBoundary = String(format: "Boundary+%08X%08X", arc4random(), arc4random())
var multipartData = Data()
{{#formParams}}
{{> toMultipartFormDataAppend}}
{{/formParams}}
multipartData.append("\r\n--\(multipartBoundary)--\r\n".data(using: .utf8) ?? Data())
request.httpBody = multipartData
request.setValue("\(multipartData.count)", forHTTPHeaderField: "Content-Length")
request.setValue("multipart/form-data; boundary=\(multipartBoundary)", forHTTPHeaderField: "Content-Type")
{{/isMultipart}}
{{^isMultipart}}
{{! Begin form urlencoded }}
var formEncodedItems: [String] = []
{{#formParams}}
{{#required}}formEncodedItems.append("{{baseName}}=\({{> toString}})"){{/required}}{{^required}}if let {{paramName}} = {{paramName}} { formEncodedItems.append("{{baseName}}=\({{> toString}})") } {{/required}}
{{/formParams}}
request.httpBody = formEncodedItems.joined(separator: "&").data(using: .utf8)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
{{/isMultipart}}
{{/hasFormParams}}
return request
}.publisher
}.flatMap { request -> AnyPublisher<{{{returnType}}}{{^returnType}}Void{{/returnType}}, Error> in
return self.transport.send(request: request)
{{#vendorExtensions.x-swift-custom-error-type}}
.mapError { transportError -> Error in
{{#responses}}
{{#vendorExtensions.x-swift-has-custom-error-type}}
if transportError.statusCode == {{{code}}} {
{{#dataType}}
do {
let error = try self.decoder.decode({{{dataType}}}.self, from: transportError.data)
return {{{vendorExtensions.x-swift-custom-error-type}}}.code{{{code}}}Error(error)
} catch {
return error
}
{{/dataType}}
{{^dataType}}
return {{{vendorExtensions.x-swift-custom-error-type}}}.code{{{code}}}Error
{{/dataType}}
}
{{/vendorExtensions.x-swift-has-custom-error-type}}
{{/responses}}
return transportError
}
{{/vendorExtensions.x-swift-custom-error-type}}
.tryMap { response in
{{#returnType}}
{{#vendorExtensions.x-swift-is-not-codable}}
if let object = try JSONSerialization.jsonObject(with: response.data, options: []) as? {{{returnType}}} {
return object
} else {
throw OpenAPITransportError.invalidResponseMappingError(data: response.data)
}
{{/vendorExtensions.x-swift-is-not-codable}}
{{^vendorExtensions.x-swift-is-not-codable}}
try self.decoder.decode({{{returnType}}}.self, from: response.data)
{{/vendorExtensions.x-swift-is-not-codable}}
{{/returnType}}
{{^returnType}}
return ()
{{/returnType}}
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
{{/operation}}
}
{{/operations}}

View File

@ -0,0 +1,35 @@
{{#models}}{{#model}}//
// {{classname}}.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
//
import Foundation
{{#description}}
/// {{.}}
{{/description}}
{{#vendorExtensions.x-is-one-of-interface}}
{{> modelOneOf}}
{{/vendorExtensions.x-is-one-of-interface}}
{{^vendorExtensions.x-is-one-of-interface}}
{{#isArray}}
{{> modelArray}}
{{/isArray}}
{{^isArray}}
{{#isEnum}}
{{> modelEnum}}
{{/isEnum}}
{{^isEnum}}
{{#vendorExtensions.x-swift-contains-not-codable}}
{{> modelObjectCustom}}
{{/vendorExtensions.x-swift-contains-not-codable}}
{{^vendorExtensions.x-swift-contains-not-codable}}
{{> modelObject}}
{{/vendorExtensions.x-swift-contains-not-codable}}
{{/isEnum}}
{{/isArray}}
{{/vendorExtensions.x-is-one-of-interface}}
{{/model}}
{{/models}}

View File

@ -0,0 +1 @@
public typealias {{classname}} = {{parent}}

View File

@ -0,0 +1,7 @@
public enum {{classname}}: {{dataType}}, Codable, CaseIterable {
{{#allowableValues}}
{{#enumVars}}
case {{{name}}} = {{{value}}}
{{/enumVars}}
{{/allowableValues}}
}

View File

@ -0,0 +1,7 @@
public enum {{enumName}}: {{^isContainer}}{{dataType}}{{/isContainer}}{{#isContainer}}String{{/isContainer}}, Codable, CaseIterable {
{{#allowableValues}}
{{#enumVars}}
case {{{name}}} = {{{value}}}
{{/enumVars}}
{{/allowableValues}}
}

View File

@ -0,0 +1,28 @@
{{#isDeprecated}}
@available(*, deprecated, message: "Deprecated API parameter")
{{/isDeprecated}}
public struct {{{classname}}}: Codable {
{{#allVars}}
{{#isEnum}}
{{> modelInlineEnumDeclaration}}
{{/isEnum}}
{{/allVars}}
{{#allVars}}
{{#isEnum}}
{{#description}}/// {{{.}}}
{{/description}}public var {{{name}}}: {{{datatypeWithEnum}}}{{#required}}{{#isNullable}}?{{/isNullable}}{{/required}}{{^required}}?{{/required}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}
{{/isEnum}}
{{^isEnum}}
{{#description}}/// {{{.}}}
{{/description}}public var {{{name}}}: {{{datatype}}}{{#required}}{{#isNullable}}?{{/isNullable}}{{/required}}{{^required}}?{{/required}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}
{{/isEnum}}
{{/allVars}}
{{#hasVars}}
public init({{#allVars}}{{{name}}}: {{{datatypeWithEnum}}}{{#required}}{{#isNullable}}?{{/isNullable}}{{/required}}{{^required}}?{{/required}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{^defaultValue}}{{^required}} = nil{{/required}}{{/defaultValue}}{{^-last}}, {{/-last}}{{/allVars}}) {
{{#allVars}}
self.{{{name}}} = {{{name}}}
{{/allVars}}
}
{{/hasVars}}
}

View File

@ -0,0 +1,66 @@
{{#isDeprecated}}
@available(*, deprecated, message: "Deprecated API parameter")
{{/isDeprecated}}
public struct {{{classname}}}: Codable {
{{#allVars}}
{{#isEnum}}
{{> modelInlineEnumDeclaration}}
{{/isEnum}}
{{/allVars}}
{{#allVars}}
{{#isEnum}}
{{#description}}/// {{{.}}}
{{/description}}public var {{{name}}}: {{{datatypeWithEnum}}}{{#required}}{{#isNullable}}?{{/isNullable}}{{/required}}{{^required}}?{{/required}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}
{{/isEnum}}
{{^isEnum}}
{{#description}}/// {{{.}}}
{{/description}}public var {{{name}}}: {{{datatype}}}{{#required}}{{#isNullable}}?{{/isNullable}}{{/required}}{{^required}}?{{/required}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}
{{/isEnum}}
{{/allVars}}
{{#hasVars}}
public init({{#allVars}}{{{name}}}: {{{datatypeWithEnum}}}{{#required}}{{#isNullable}}?{{/isNullable}}{{/required}}{{^required}}?{{/required}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{^defaultValue}}{{^required}} = nil{{/required}}{{/defaultValue}}{{^-last}}, {{/-last}}{{/allVars}}) {
{{#allVars}}
self.{{{name}}} = {{{name}}}
{{/allVars}}
}
{{/hasVars}}
public enum CodingKeys: {{#hasVars}}String, {{/hasVars}}CodingKey, CaseIterable {
{{#allVars}}
case {{{name}}}{{#vendorExtensions.x-codegen-escaped-property-name}} = "{{{baseName}}}"{{/vendorExtensions.x-codegen-escaped-property-name}}
{{/allVars}}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
{{#allVars}}
{{{name}}} = try container.decode{{^required}}IfPresent{{/required}}({{{datatypeWithEnum}}}.self, forKey: .{{{name}}})
{{/allVars}}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
{{#allVars}}
{{#vendorExtensions.x-swift-is-not-codable}}
{{#required}}
try container.encode(try JSONSerialization.data(withJSONObject: {{{name}}}), forKey: .{{{name}}})
{{/required}}
{{^required}}
if let {{{name}}} = {{{name}}} {
try container.encodeIfPresent(try JSONSerialization.data(withJSONObject: {{{name}}}), forKey: .{{{name}}})
}
{{/required}}
{{/vendorExtensions.x-swift-is-not-codable}}
{{^vendorExtensions.x-swift-is-not-codable}}
try container.encode{{^required}}IfPresent{{/required}}({{{name}}}, forKey: .{{{name}}})
{{/vendorExtensions.x-swift-is-not-codable}}
{{/allVars}}
{{#generateModelAdditionalProperties}}
{{#additionalPropertiesType}}
var additionalPropertiesContainer = encoder.container(keyedBy: String.self)
try additionalPropertiesContainer.encodeMap(additionalProperties)
{{/additionalPropertiesType}}
{{/generateModelAdditionalProperties}}
}
}

View File

@ -0,0 +1,34 @@
{{#isDeprecated}}
@available(*, deprecated, message: "Deprecated API parameter")
{{/isDeprecated}}
public enum {{classname}}: Codable {
{{#oneOf}}
case type{{.}}({{.}})
{{/oneOf}}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
{{#oneOf}}
case .type{{.}}(let value):
try container.encode(value)
{{/oneOf}}
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
{{#oneOf}}
{{#-first}}
if let value = try? container.decode({{.}}.self) {
{{/-first}}
{{^-first}}
} else if let value = try? container.decode({{.}}.self) {
{{/-first}}
self = .type{{.}}(value)
{{/oneOf}}
} else {
throw DecodingError.typeMismatch(Self.Type.self, .init(codingPath: decoder.codingPath, debugDescription: "Unable to decode instance of {{classname}}"))
}
}
}

View File

@ -0,0 +1 @@
{{#isBinary}}{{#mapFileBinaryToData}}{{{paramName}}}{{/mapFileBinaryToData}}{{^mapFileBinaryToData}}try Data(contentsOf: {{{paramName}}}){{/mapFileBinaryToData}}{{/isBinary}}{{#isModel}}try self.encoder.encode({{paramName}}){{/isModel}}{{#isByteArray}}{{paramName}}.base64EncodedData(){{/isByteArray}}{{#isString}}{{paramName}}{{#isEnum}}.rawValue{{/isEnum}}.data(using: .utf8) ?? Data(){{/isString}}

View File

@ -0,0 +1,59 @@
{{#vendorExtensions.x-swift-enumerate-multipart}}
{{^mapFileBinaryToData}}try {{/mapFileBinaryToData}}{{paramName}}{{^required}}?{{/required}}.enumerated().forEach { index, {{paramName}} in
{{#mapFileBinaryToData}}
let filename = "\({{paramName}})\(index)"
{{/mapFileBinaryToData}}
{{^mapFileBinaryToData}}
let filename = {{paramName}}.lastPathComponent
{{/mapFileBinaryToData}}
let {{paramName}}Header = "--\(multipartBoundary)\r\n"
.appending("Content-Disposition:form-data; name=\"{{paramName}}\"; filename=\"\(filename)\"\r\n")
{{#contentType}}
.appending("Content-Type: {{{contentType}}}\r\n")
{{/contentType}}
.appending("\r\n")
multipartData.append({{paramName}}Header.data(using: .utf8) ?? Data())
multipartData.append({{#items}}{{> toData}}{{/items}})
multipartData.append("\r\n".data(using: .utf8) ?? Data())
}
{{/vendorExtensions.x-swift-enumerate-multipart}}
{{^vendorExtensions.x-swift-enumerate-multipart}}
{{#required}}
let {{paramName}}Header = "--\(multipartBoundary)\r\n"
{{#isFile}}
.appending("Content-Disposition:form-data; name=\"{{paramName}}\"; filename=\"{{paramName}}\"\r\n")
{{/isFile}}
{{^isFile}}
.appending("Content-Disposition:form-data; name=\"{{paramName}}\"\r\n")
{{/isFile}}
{{#contentType}}
.appending("Content-Type: {{{contentType}}}\r\n")
{{/contentType}}
.appending("\r\n")
multipartData.append({{paramName}}Header.data(using: .utf8) ?? Data())
multipartData.append({{> toData}})
{{^-last}}
multipartData.append("\r\n".data(using: .utf8) ?? Data())
{{/-last}}
{{/required}}
{{^required}}
if let {{paramName}} = {{paramName}} {
let {{paramName}}Header = "--\(multipartBoundary)\r\n"
{{#isFile}}
.appending("Content-Disposition:form-data; name=\"{{paramName}}\"; filename=\"{{paramName}}\"\r\n")
{{/isFile}}
{{^isFile}}
.appending("Content-Disposition:form-data; name=\"{{paramName}}\"\r\n")
{{/isFile}}
{{#contentType}}
.appending("Content-Type: {{{contentType}}}\r\n")
{{/contentType}}
.appending("\r\n")
multipartData.append({{paramName}}Header.data(using: .utf8) ?? Data())
multipartData.append({{> toData}})
{{^-last}}
multipartData.append("\r\n".data(using: .utf8) ?? Data())
{{/-last}}
}
{{/required}}
{{/vendorExtensions.x-swift-enumerate-multipart}}

View File

@ -0,0 +1 @@
{{#isDateTime}}OpenISO8601DateFormatter.shared.string(from: {{paramName}}){{/isDateTime}}{{#vendorExtensions.x-swift-use-encoder}}String(data: try self.encoder.encode({{paramName}}), encoding: .utf8) ?? ""{{/vendorExtensions.x-swift-use-encoder}}{{^vendorExtensions.x-swift-use-encoder}}{{#isArray}}{{paramName}}{{#vendorExtensions.x-swift-is-base-type-udid}}.map { $0.uuidString }{{/vendorExtensions.x-swift-is-base-type-udid}}{{#vendorExtensions.x-swift-is-base-type-enum}}.map { $0.rawValue }{{/vendorExtensions.x-swift-is-base-type-enum}}.joined(separator: ","){{/isArray}}{{^isArray}}{{#vendorExtensions.x-swift-is-enum-type}}{{paramName}}.rawValue{{/vendorExtensions.x-swift-is-enum-type}}{{^isEnum}}{{#isString}}{{#isUuid}}{{paramName}}.uuidString{{/isUuid}}{{^isUuid}}{{paramName}}{{/isUuid}}{{/isString}}{{#isInteger}}"\({{paramName}})"{{/isInteger}}{{#isDouble}}"\({{paramName}})"{{/isDouble}}{{#isFloat}}"\({{paramName}})"{{/isFloat}}{{#isNumber}}"\({{paramName}})"{{/isNumber}}{{#isLong}}"\({{paramName}})"{{/isLong}}{{#isBoolean}}{{paramName}} ? "true" : "false"{{/isBoolean}}{{/isEnum}}{{/isArray}}{{/vendorExtensions.x-swift-use-encoder}}

View File

@ -0,0 +1,30 @@
package org.openapitools.codegen.options;
import org.openapitools.codegen.languages.SwiftCombineClientCodegen;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
public class SwiftCombineClientCodegenOptionsProvider implements OptionsProvider {
public static final String PROJECT_NAME_VALUE = "OpenAPI";
@Override
public String getLanguage() {
return "swift-combine";
}
@Override
public Map<String, String> createOptions() {
ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<String, String>();
return builder
.put(SwiftCombineClientCodegen.PROJECT_NAME, PROJECT_NAME_VALUE)
.build();
}
@Override
public boolean isServer() {
return false;
}
}

View File

@ -0,0 +1,64 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## Build generated
build/
DerivedData
## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
## Other
*.xccheckout
*.moved-aside
*.xcuserstate
*.xcscmblueprint
## Obj-C/Swift specific
*.hmap
*.ipa
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
.build/
.swiftpm
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md
fastlane/report.xml
fastlane/screenshots

View File

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

View File

@ -0,0 +1,13 @@
OpenAPITransport/Package.swift
OpenAPITransport/Sources/OpenAPITransport.swift
PetstoreOpenAPI/Package.swift
PetstoreOpenAPI/Sources/APIs/PetAPI.swift
PetstoreOpenAPI/Sources/APIs/StoreAPI.swift
PetstoreOpenAPI/Sources/APIs/UserAPI.swift
PetstoreOpenAPI/Sources/Models/ApiResponse.swift
PetstoreOpenAPI/Sources/Models/Category.swift
PetstoreOpenAPI/Sources/Models/Order.swift
PetstoreOpenAPI/Sources/Models/Pet.swift
PetstoreOpenAPI/Sources/Models/Tag.swift
PetstoreOpenAPI/Sources/Models/User.swift
PetstoreOpenAPI/Sources/Private/OpenISO8601DateFormatter.swift

View File

@ -0,0 +1 @@
7.0.0-SNAPSHOT

View File

@ -0,0 +1,25 @@
// swift-tools-version:5.1
import PackageDescription
let package = Package(
name: "OpenAPITransport",
platforms: [
.iOS(.v13),
.macOS(.v10_15)
],
products: [
.library(
name: "OpenAPITransport",
targets: ["OpenAPITransport"]
),
],
dependencies: [],
targets: [
.target(
name: "OpenAPITransport",
dependencies: [],
path: "Sources"
),
]
)

View File

@ -0,0 +1,306 @@
// OpenAPITransport.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
import Foundation
import Combine
// MARK: - OpenAPITransport
public protocol OpenAPITransport: AnyObject {
var baseURL: URL? { get }
func send(request: URLRequest) -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError>
func cancelAll()
}
public struct OpenAPITransportResponse {
public let data: Data
public let statusCode: Int
public init(data: Data, statusCode: Int) {
self.data = data
self.statusCode = statusCode
}
}
public struct OpenAPITransportError: Error, CustomStringConvertible, LocalizedError {
public let statusCode: Int
public let description: String
public let errorDescription: String?
/// It might be source network error
public let nestedError: Error?
/// Data may contain additional reason info (like json payload)
public let data: Data
public init(
statusCode: Int,
description: String? = nil,
errorDescription: String? = nil,
nestedError: Error? = nil,
data: Data = Data()
) {
self.statusCode = statusCode
self.errorDescription = errorDescription
self.nestedError = nestedError
self.data = data
if let description = description {
self.description = description
} else {
var summary = "OpenAPITransportError with status \(statusCode)"
if let nestedError = nestedError {
summary.append(contentsOf: ", \(nestedError.localizedDescription)")
}
self.description = summary
}
}
}
// MARK: - Policy
/// Policy to define whether response is successful or requires authentication
public protocol ResponsePolicy {
func defineState(for request: URLRequest, output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<ResponseState, Never>
}
public enum ResponseState {
/// Return success to client
case success
/// Return error to client
case failure
/// Repeat request
case retry
}
// MARK: - Interceptor
/// Define how to customize URL request before network call
public protocol Interceptor {
/// Customize request before performing. Add headers or encrypt body for example.
func intercept(request: URLRequest) -> AnyPublisher<URLRequest, OpenAPITransportError>
/// Customize response before handling. Decrypt body for example.
func intercept(output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<URLSession.DataTaskPublisher.Output, OpenAPITransportError>
}
// MARK: - Transport delegate
public protocol OpenAPITransportDelegate: AnyObject {
func willStart(request: URLRequest)
func didFinish(request: URLRequest, response: HTTPURLResponse?, data: Data)
func didFinish(request: URLRequest, error: Error)
}
// MARK: - Implementation
open class URLSessionOpenAPITransport: OpenAPITransport {
public struct Config {
public var baseURL: URL?
public var session: URLSession
public var processor: Interceptor
public var policy: ResponsePolicy
public weak var delegate: OpenAPITransportDelegate?
public init(
baseURL: URL? = nil,
session: URLSession = .shared,
processor: Interceptor = DefaultInterceptor(),
policy: ResponsePolicy = DefaultResponsePolicy(),
delegate: OpenAPITransportDelegate? = nil
) {
self.baseURL = baseURL
self.session = session
self.processor = processor
self.policy = policy
self.delegate = delegate
}
}
private var cancellable = Set<AnyCancellable>()
public var config: Config
public var baseURL: URL? { config.baseURL }
public init(config: Config = .init()) {
self.config = config
}
open func send(request: URLRequest) -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> {
config.processor
// Add custom headers or refresh token if needed
.intercept(request: request)
.flatMap { request -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> in
self.config.delegate?.willStart(request: request)
// Perform network call
return self.config.session.dataTaskPublisher(for: request)
.mapError {
self.config.delegate?.didFinish(request: request, error: $0)
return OpenAPITransportError(statusCode: $0.code.rawValue, description: "Network call finished fails")
}
.flatMap { output in
self.config.processor.intercept(output: output)
}
.flatMap { output -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> in
let response = output.response as? HTTPURLResponse
self.config.delegate?.didFinish(request: request, response: response, data: output.data)
return self.config.policy.defineState(for: request, output: output)
.setFailureType(to: OpenAPITransportError.self)
.flatMap { state -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> in
switch state {
case .success:
let transportResponse = OpenAPITransportResponse(data: output.data, statusCode: 200)
return Result.success(transportResponse).publisher.eraseToAnyPublisher()
case .retry:
return Fail(error: OpenAPITransportError.retryError).eraseToAnyPublisher()
case .failure:
let code = response?.statusCode ?? OpenAPITransportError.noResponseCode
let transportError = OpenAPITransportError(statusCode: code, data: output.data)
return Fail(error: transportError).eraseToAnyPublisher()
}
}.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
.retry(times: 2) { error -> Bool in
return error.statusCode == OpenAPITransportError.retryError.statusCode
}.eraseToAnyPublisher()
}
open func cancelAll() {
cancellable.removeAll()
}
}
public final class DefaultInterceptor: Interceptor {
public init() {}
public func intercept(request: URLRequest) -> AnyPublisher<URLRequest, OpenAPITransportError> {
Just(request)
.setFailureType(to: OpenAPITransportError.self)
.eraseToAnyPublisher()
}
public func intercept(output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<URLSession.DataTaskPublisher.Output, OpenAPITransportError> {
Just(output)
.setFailureType(to: OpenAPITransportError.self)
.eraseToAnyPublisher()
}
}
public final class DefaultResponsePolicy: ResponsePolicy {
public init() {}
public func defineState(for request: URLRequest, output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<ResponseState, Never> {
let state: ResponseState
switch (output.response as? HTTPURLResponse)?.statusCode {
case .some(200...299): state = .success
default: state = .failure
}
return Just(state).eraseToAnyPublisher()
}
}
/// Custom transport errors. It begins with 6.. not to conflict with HTTP codes
public extension OpenAPITransportError {
static let incorrectAuthenticationCode = 600
static func incorrectAuthenticationError(_ nestedError: Error? = nil) -> OpenAPITransportError {
OpenAPITransportError(
statusCode: OpenAPITransportError.incorrectAuthenticationCode,
description: "Impossible to add authentication headers to request",
errorDescription: NSLocalizedString(
"Impossible to add authentication headers to request",
comment: "Incorrect authentication"
),
nestedError: nestedError
)
}
static let failedAuthenticationRefreshCode = 601
static func failedAuthenticationRefreshError(_ nestedError: Error? = nil) -> OpenAPITransportError {
OpenAPITransportError(
statusCode: OpenAPITransportError.failedAuthenticationRefreshCode,
description: "Error while refreshing authentication",
errorDescription: NSLocalizedString(
"Error while refreshing authentication",
comment: "Failed authentication refresh"
),
nestedError: nestedError
)
}
static let noResponseCode = 603
static func noResponseError(_ nestedError: Error? = nil) -> OpenAPITransportError {
OpenAPITransportError(
statusCode: OpenAPITransportError.noResponseCode,
description: "There is no HTTP URL response",
errorDescription: NSLocalizedString(
"There is no HTTP URL response",
comment: "No response"
),
nestedError: nestedError
)
}
static let badURLCode = 604
static func badURLError(_ nestedError: Error? = nil) -> OpenAPITransportError {
OpenAPITransportError(
statusCode: OpenAPITransportError.badURLCode,
description: "Request URL cannot be created with given parameters",
errorDescription: NSLocalizedString(
"Request URL cannot be created with given parameters",
comment: "Bad URL"
),
nestedError: nestedError
)
}
static let invalidResponseMappingCode = 605
static func invalidResponseMappingError(data: Data) -> OpenAPITransportError {
OpenAPITransportError(
statusCode: OpenAPITransportError.invalidResponseMappingCode,
description: "Response data cannot be expected object scheme",
errorDescription: NSLocalizedString(
"Response data cannot be expected object scheme",
comment: "Invalid response mapping"
),
data: data
)
}
static let retryErrorCode = 606
static let retryError = OpenAPITransportError(statusCode: OpenAPITransportError.retryErrorCode)
}
// MARK: - Private
private extension Publishers {
struct RetryIf<P: Publisher>: Publisher {
typealias Output = P.Output
typealias Failure = P.Failure
let publisher: P
let times: Int
let condition: (P.Failure) -> Bool
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
guard times > 0 else { return publisher.receive(subscriber: subscriber) }
publisher.catch { (error: P.Failure) -> AnyPublisher<Output, Failure> in
if condition(error) {
return RetryIf(publisher: publisher, times: times - 1, condition: condition).eraseToAnyPublisher()
} else {
return Fail(error: error).eraseToAnyPublisher()
}
}.receive(subscriber: subscriber)
}
}
}
private extension Publisher {
func retry(times: Int, if condition: @escaping (Failure) -> Bool) -> Publishers.RetryIf<Self> {
Publishers.RetryIf(publisher: self, times: times, condition: condition)
}
}

View File

@ -0,0 +1,25 @@
// swift-tools-version:5.1
import PackageDescription
let package = Package(
name: "PetstoreOpenAPI",
platforms: [
.iOS(.v13),
.macOS(.v10_15)
],
products: [
.library(
name: "PetstoreOpenAPI",
targets: ["PetstoreOpenAPI"]
),
],
dependencies: [.package(path: "../OpenAPITransport")],
targets: [
.target(
name: "PetstoreOpenAPI",
dependencies: [.byName(name: "OpenAPITransport")],
path: "Sources"
),
]
)

View File

@ -0,0 +1,503 @@
//
// PetAPI.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
import Foundation
import Combine
import OpenAPITransport
open class PetAPI {
private let transport: OpenAPITransport
public var encoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .formatted(OpenISO8601DateFormatter())
return encoder
}()
public var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(OpenISO8601DateFormatter())
return decoder
}()
public var baseURL = URL(string: "http://petstore.swagger.io/v2")
public init(_ transport: OpenAPITransport) {
self.transport = transport
}
public enum AddPetError: Error, CustomStringConvertible {
// Invalid input
case code405Error
public var description: String {
switch self {
case .code405Error:
return "AddPetError: Invalid input"
}
}
}
/// Add a new pet to the store
/// - POST /pet
/// -
/// - OAuth:
/// - type: oauth2
/// - name: petstore_auth
/// - parameter pet: (body) Pet object that needs to be added to the store
/// - returns: AnyPublisher<Pet, Error>
open func addPet(pet: Pet) -> AnyPublisher<Pet, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
let path = "/pet"
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "POST"
request.httpBody = try self.encoder.encode(pet)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
return request
}.publisher
}.flatMap { request -> AnyPublisher<Pet, Error> in
return self.transport.send(request: request)
.mapError { transportError -> Error in
if transportError.statusCode == 405 {
return AddPetError.code405Error
}
return transportError
}
.tryMap { response in
try self.decoder.decode(Pet.self, from: response.data)
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
public enum DeletePetError: Error, CustomStringConvertible {
// Invalid pet value
case code400Error
public var description: String {
switch self {
case .code400Error:
return "DeletePetError: Invalid pet value"
}
}
}
/// Deletes a pet
/// - DELETE /pet/{petId}
/// -
/// - OAuth:
/// - type: oauth2
/// - name: petstore_auth
/// - parameter petId: (path) Pet id to delete
/// - parameter apiKey: (header) (optional)
/// - returns: AnyPublisher<Void, Error>
open func deletePet(petId: Int64, apiKey: String? = nil) -> AnyPublisher<Void, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
var path = "/pet/{petId}"
path = path.replacingOccurrences(of: "{petId}", with: "\(petId)")
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "DELETE"
var headers = [String: String]()
if let apiKey = apiKey { headers["api_key"] = apiKey }
request.allHTTPHeaderFields = headers
return request
}.publisher
}.flatMap { request -> AnyPublisher<Void, Error> in
return self.transport.send(request: request)
.mapError { transportError -> Error in
if transportError.statusCode == 400 {
return DeletePetError.code400Error
}
return transportError
}
.tryMap { response in
return ()
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
///
/// Enum for parameter status
///
public enum FindPetsByStatusStatus: String, Codable, CaseIterable {
case available = "available"
case pending = "pending"
case sold = "sold"
}
public enum FindPetsByStatusError: Error, CustomStringConvertible {
// Invalid status value
case code400Error
public var description: String {
switch self {
case .code400Error:
return "FindPetsByStatusError: Invalid status value"
}
}
}
/// Finds Pets by status
/// - GET /pet/findByStatus
/// - Multiple status values can be provided with comma separated strings
/// - OAuth:
/// - type: oauth2
/// - name: petstore_auth
/// - parameter status: (query) Status values that need to be considered for filter
/// - returns: AnyPublisher<[Pet], Error>
open func findPetsByStatus(status: [FindPetsByStatusStatus]) -> AnyPublisher<[Pet], Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
let path = "/pet/findByStatus"
let url = baseURL.appendingPathComponent(path)
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
var queryItems: [URLQueryItem] = []
queryItems.append(URLQueryItem(name: "status", value: status.map { $0.rawValue }.joined(separator: ",")))
components?.queryItems = queryItems
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "GET"
return request
}.publisher
}.flatMap { request -> AnyPublisher<[Pet], Error> in
return self.transport.send(request: request)
.mapError { transportError -> Error in
if transportError.statusCode == 400 {
return FindPetsByStatusError.code400Error
}
return transportError
}
.tryMap { response in
try self.decoder.decode([Pet].self, from: response.data)
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
public enum FindPetsByTagsError: Error, CustomStringConvertible {
// Invalid tag value
case code400Error
public var description: String {
switch self {
case .code400Error:
return "FindPetsByTagsError: Invalid tag value"
}
}
}
/// Finds Pets by tags
/// - GET /pet/findByTags
/// - Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
/// - OAuth:
/// - type: oauth2
/// - name: petstore_auth
/// - parameter tags: (query) Tags to filter by
/// - returns: AnyPublisher<[Pet], Error>
@available(*, deprecated, message: "Deprecated API operation")
open func findPetsByTags(tags: [String]) -> AnyPublisher<[Pet], Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
let path = "/pet/findByTags"
let url = baseURL.appendingPathComponent(path)
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
var queryItems: [URLQueryItem] = []
queryItems.append(URLQueryItem(name: "tags", value: tags.joined(separator: ",")))
components?.queryItems = queryItems
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "GET"
return request
}.publisher
}.flatMap { request -> AnyPublisher<[Pet], Error> in
return self.transport.send(request: request)
.mapError { transportError -> Error in
if transportError.statusCode == 400 {
return FindPetsByTagsError.code400Error
}
return transportError
}
.tryMap { response in
try self.decoder.decode([Pet].self, from: response.data)
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
public enum GetPetByIdError: Error, CustomStringConvertible {
// Invalid ID supplied
case code400Error
// Pet not found
case code404Error
public var description: String {
switch self {
case .code400Error:
return "GetPetByIdError: Invalid ID supplied"
case .code404Error:
return "GetPetByIdError: Pet not found"
}
}
}
/// Find pet by ID
/// - GET /pet/{petId}
/// - Returns a single pet
/// - API Key:
/// - type: apiKey api_key
/// - name: api_key
/// - parameter petId: (path) ID of pet to return
/// - returns: AnyPublisher<Pet, Error>
open func getPetById(petId: Int64) -> AnyPublisher<Pet, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
var path = "/pet/{petId}"
path = path.replacingOccurrences(of: "{petId}", with: "\(petId)")
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "GET"
return request
}.publisher
}.flatMap { request -> AnyPublisher<Pet, Error> in
return self.transport.send(request: request)
.mapError { transportError -> Error in
if transportError.statusCode == 400 {
return GetPetByIdError.code400Error
}
if transportError.statusCode == 404 {
return GetPetByIdError.code404Error
}
return transportError
}
.tryMap { response in
try self.decoder.decode(Pet.self, from: response.data)
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
public enum UpdatePetError: Error, CustomStringConvertible {
// Invalid ID supplied
case code400Error
// Pet not found
case code404Error
// Validation exception
case code405Error
public var description: String {
switch self {
case .code400Error:
return "UpdatePetError: Invalid ID supplied"
case .code404Error:
return "UpdatePetError: Pet not found"
case .code405Error:
return "UpdatePetError: Validation exception"
}
}
}
/// Update an existing pet
/// - PUT /pet
/// -
/// - OAuth:
/// - type: oauth2
/// - name: petstore_auth
/// - externalDocs:
/// url: http://petstore.swagger.io/v2/doc/updatePet
/// description: API documentation for the updatePet operation
/// - parameter pet: (body) Pet object that needs to be added to the store
/// - returns: AnyPublisher<Pet, Error>
open func updatePet(pet: Pet) -> AnyPublisher<Pet, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
let path = "/pet"
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "PUT"
request.httpBody = try self.encoder.encode(pet)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
return request
}.publisher
}.flatMap { request -> AnyPublisher<Pet, Error> in
return self.transport.send(request: request)
.mapError { transportError -> Error in
if transportError.statusCode == 400 {
return UpdatePetError.code400Error
}
if transportError.statusCode == 404 {
return UpdatePetError.code404Error
}
if transportError.statusCode == 405 {
return UpdatePetError.code405Error
}
return transportError
}
.tryMap { response in
try self.decoder.decode(Pet.self, from: response.data)
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
public enum UpdatePetWithFormError: Error, CustomStringConvertible {
// Invalid input
case code405Error
public var description: String {
switch self {
case .code405Error:
return "UpdatePetWithFormError: Invalid input"
}
}
}
/// Updates a pet in the store with form data
/// - POST /pet/{petId}
/// -
/// - OAuth:
/// - type: oauth2
/// - name: petstore_auth
/// - parameter petId: (path) ID of pet that needs to be updated
/// - parameter name: (form) Updated name of the pet (optional)
/// - parameter status: (form) Updated status of the pet (optional)
/// - returns: AnyPublisher<Void, Error>
open func updatePetWithForm(petId: Int64, name: String? = nil, status: String? = nil) -> AnyPublisher<Void, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
var path = "/pet/{petId}"
path = path.replacingOccurrences(of: "{petId}", with: "\(petId)")
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "POST"
var formEncodedItems: [String] = []
if let name = name { formEncodedItems.append("name=\(name)") }
if let status = status { formEncodedItems.append("status=\(status)") }
request.httpBody = formEncodedItems.joined(separator: "&").data(using: .utf8)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
return request
}.publisher
}.flatMap { request -> AnyPublisher<Void, Error> in
return self.transport.send(request: request)
.mapError { transportError -> Error in
if transportError.statusCode == 405 {
return UpdatePetWithFormError.code405Error
}
return transportError
}
.tryMap { response in
return ()
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
/// uploads an image
/// - POST /pet/{petId}/uploadImage
/// -
/// - OAuth:
/// - type: oauth2
/// - name: petstore_auth
/// - parameter petId: (path) ID of pet to update
/// - parameter additionalMetadata: (form) Additional data to pass to server (optional)
/// - parameter file: (form) file to upload (optional)
/// - returns: AnyPublisher<ApiResponse, Error>
open func uploadFile(petId: Int64, additionalMetadata: String? = nil, file: Data? = nil) -> AnyPublisher<ApiResponse, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
var path = "/pet/{petId}/uploadImage"
path = path.replacingOccurrences(of: "{petId}", with: "\(petId)")
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "POST"
let multipartBoundary = String(format: "Boundary+%08X%08X", arc4random(), arc4random())
var multipartData = Data()
if let additionalMetadata = additionalMetadata {
let additionalMetadataHeader = "--\(multipartBoundary)\r\n"
.appending("Content-Disposition:form-data; name=\"additionalMetadata\"\r\n")
.appending("\r\n")
multipartData.append(additionalMetadataHeader.data(using: .utf8) ?? Data())
multipartData.append(additionalMetadata.data(using: .utf8) ?? Data())
multipartData.append("\r\n".data(using: .utf8) ?? Data())
}
if let file = file {
let fileHeader = "--\(multipartBoundary)\r\n"
.appending("Content-Disposition:form-data; name=\"file\"; filename=\"file\"\r\n")
.appending("\r\n")
multipartData.append(fileHeader.data(using: .utf8) ?? Data())
multipartData.append(file)
}
multipartData.append("\r\n--\(multipartBoundary)--\r\n".data(using: .utf8) ?? Data())
request.httpBody = multipartData
request.setValue("\(multipartData.count)", forHTTPHeaderField: "Content-Length")
request.setValue("multipart/form-data; boundary=\(multipartBoundary)", forHTTPHeaderField: "Content-Type")
return request
}.publisher
}.flatMap { request -> AnyPublisher<ApiResponse, Error> in
return self.transport.send(request: request)
.tryMap { response in
try self.decoder.decode(ApiResponse.self, from: response.data)
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
}

View File

@ -0,0 +1,224 @@
//
// StoreAPI.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
import Foundation
import Combine
import OpenAPITransport
open class StoreAPI {
private let transport: OpenAPITransport
public var encoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .formatted(OpenISO8601DateFormatter())
return encoder
}()
public var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(OpenISO8601DateFormatter())
return decoder
}()
public var baseURL = URL(string: "http://petstore.swagger.io/v2")
public init(_ transport: OpenAPITransport) {
self.transport = transport
}
public enum DeleteOrderError: Error, CustomStringConvertible {
// Invalid ID supplied
case code400Error
// Order not found
case code404Error
public var description: String {
switch self {
case .code400Error:
return "DeleteOrderError: Invalid ID supplied"
case .code404Error:
return "DeleteOrderError: Order not found"
}
}
}
/// Delete purchase order by ID
/// - DELETE /store/order/{orderId}
/// - For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
/// - parameter orderId: (path) ID of the order that needs to be deleted
/// - returns: AnyPublisher<Void, Error>
open func deleteOrder(orderId: String) -> AnyPublisher<Void, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
var path = "/store/order/{orderId}"
path = path.replacingOccurrences(of: "{orderId}", with: orderId)
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "DELETE"
return request
}.publisher
}.flatMap { request -> AnyPublisher<Void, Error> in
return self.transport.send(request: request)
.mapError { transportError -> Error in
if transportError.statusCode == 400 {
return DeleteOrderError.code400Error
}
if transportError.statusCode == 404 {
return DeleteOrderError.code404Error
}
return transportError
}
.tryMap { response in
return ()
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
/// Returns pet inventories by status
/// - GET /store/inventory
/// - Returns a map of status codes to quantities
/// - API Key:
/// - type: apiKey api_key
/// - name: api_key
/// - returns: AnyPublisher<[String: Int], Error>
open func getInventory() -> AnyPublisher<[String: Int], Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
let path = "/store/inventory"
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "GET"
return request
}.publisher
}.flatMap { request -> AnyPublisher<[String: Int], Error> in
return self.transport.send(request: request)
.tryMap { response in
try self.decoder.decode([String: Int].self, from: response.data)
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
public enum GetOrderByIdError: Error, CustomStringConvertible {
// Invalid ID supplied
case code400Error
// Order not found
case code404Error
public var description: String {
switch self {
case .code400Error:
return "GetOrderByIdError: Invalid ID supplied"
case .code404Error:
return "GetOrderByIdError: Order not found"
}
}
}
/// Find purchase order by ID
/// - GET /store/order/{orderId}
/// - For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions
/// - parameter orderId: (path) ID of pet that needs to be fetched
/// - returns: AnyPublisher<Order, Error>
open func getOrderById(orderId: Int64) -> AnyPublisher<Order, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
var path = "/store/order/{orderId}"
path = path.replacingOccurrences(of: "{orderId}", with: "\(orderId)")
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "GET"
return request
}.publisher
}.flatMap { request -> AnyPublisher<Order, Error> in
return self.transport.send(request: request)
.mapError { transportError -> Error in
if transportError.statusCode == 400 {
return GetOrderByIdError.code400Error
}
if transportError.statusCode == 404 {
return GetOrderByIdError.code404Error
}
return transportError
}
.tryMap { response in
try self.decoder.decode(Order.self, from: response.data)
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
public enum PlaceOrderError: Error, CustomStringConvertible {
// Invalid Order
case code400Error
public var description: String {
switch self {
case .code400Error:
return "PlaceOrderError: Invalid Order"
}
}
}
/// Place an order for a pet
/// - POST /store/order
/// -
/// - parameter order: (body) order placed for purchasing the pet
/// - returns: AnyPublisher<Order, Error>
open func placeOrder(order: Order) -> AnyPublisher<Order, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
let path = "/store/order"
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "POST"
request.httpBody = try self.encoder.encode(order)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
return request
}.publisher
}.flatMap { request -> AnyPublisher<Order, Error> in
return self.transport.send(request: request)
.mapError { transportError -> Error in
if transportError.statusCode == 400 {
return PlaceOrderError.code400Error
}
return transportError
}
.tryMap { response in
try self.decoder.decode(Order.self, from: response.data)
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
}

View File

@ -0,0 +1,401 @@
//
// UserAPI.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
import Foundation
import Combine
import OpenAPITransport
open class UserAPI {
private let transport: OpenAPITransport
public var encoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .formatted(OpenISO8601DateFormatter())
return encoder
}()
public var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(OpenISO8601DateFormatter())
return decoder
}()
public var baseURL = URL(string: "http://petstore.swagger.io/v2")
public init(_ transport: OpenAPITransport) {
self.transport = transport
}
/// Create user
/// - POST /user
/// - This can only be done by the logged in user.
/// - API Key:
/// - type: apiKey api_key
/// - name: api_key
/// - parameter user: (body) Created user object
/// - returns: AnyPublisher<Void, Error>
open func createUser(user: User) -> AnyPublisher<Void, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
let path = "/user"
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "POST"
request.httpBody = try self.encoder.encode(user)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
return request
}.publisher
}.flatMap { request -> AnyPublisher<Void, Error> in
return self.transport.send(request: request)
.tryMap { response in
return ()
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
/// Creates list of users with given input array
/// - POST /user/createWithArray
/// -
/// - API Key:
/// - type: apiKey api_key
/// - name: api_key
/// - parameter user: (body) List of user object
/// - returns: AnyPublisher<Void, Error>
open func createUsersWithArrayInput(user: [User]) -> AnyPublisher<Void, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
let path = "/user/createWithArray"
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "POST"
request.httpBody = try self.encoder.encode(user)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
return request
}.publisher
}.flatMap { request -> AnyPublisher<Void, Error> in
return self.transport.send(request: request)
.tryMap { response in
return ()
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
/// Creates list of users with given input array
/// - POST /user/createWithList
/// -
/// - API Key:
/// - type: apiKey api_key
/// - name: api_key
/// - parameter user: (body) List of user object
/// - returns: AnyPublisher<Void, Error>
open func createUsersWithListInput(user: [User]) -> AnyPublisher<Void, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
let path = "/user/createWithList"
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "POST"
request.httpBody = try self.encoder.encode(user)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
return request
}.publisher
}.flatMap { request -> AnyPublisher<Void, Error> in
return self.transport.send(request: request)
.tryMap { response in
return ()
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
public enum DeleteUserError: Error, CustomStringConvertible {
// Invalid username supplied
case code400Error
// User not found
case code404Error
public var description: String {
switch self {
case .code400Error:
return "DeleteUserError: Invalid username supplied"
case .code404Error:
return "DeleteUserError: User not found"
}
}
}
/// Delete user
/// - DELETE /user/{username}
/// - This can only be done by the logged in user.
/// - API Key:
/// - type: apiKey api_key
/// - name: api_key
/// - parameter username: (path) The name that needs to be deleted
/// - returns: AnyPublisher<Void, Error>
open func deleteUser(username: String) -> AnyPublisher<Void, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
var path = "/user/{username}"
path = path.replacingOccurrences(of: "{username}", with: username)
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "DELETE"
return request
}.publisher
}.flatMap { request -> AnyPublisher<Void, Error> in
return self.transport.send(request: request)
.mapError { transportError -> Error in
if transportError.statusCode == 400 {
return DeleteUserError.code400Error
}
if transportError.statusCode == 404 {
return DeleteUserError.code404Error
}
return transportError
}
.tryMap { response in
return ()
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
public enum GetUserByNameError: Error, CustomStringConvertible {
// Invalid username supplied
case code400Error
// User not found
case code404Error
public var description: String {
switch self {
case .code400Error:
return "GetUserByNameError: Invalid username supplied"
case .code404Error:
return "GetUserByNameError: User not found"
}
}
}
/// Get user by user name
/// - GET /user/{username}
/// -
/// - parameter username: (path) The name that needs to be fetched. Use user1 for testing.
/// - returns: AnyPublisher<User, Error>
open func getUserByName(username: String) -> AnyPublisher<User, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
var path = "/user/{username}"
path = path.replacingOccurrences(of: "{username}", with: username)
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "GET"
return request
}.publisher
}.flatMap { request -> AnyPublisher<User, Error> in
return self.transport.send(request: request)
.mapError { transportError -> Error in
if transportError.statusCode == 400 {
return GetUserByNameError.code400Error
}
if transportError.statusCode == 404 {
return GetUserByNameError.code404Error
}
return transportError
}
.tryMap { response in
try self.decoder.decode(User.self, from: response.data)
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
public enum LoginUserError: Error, CustomStringConvertible {
// Invalid username/password supplied
case code400Error
public var description: String {
switch self {
case .code400Error:
return "LoginUserError: Invalid username/password supplied"
}
}
}
/// Logs user into the system
/// - GET /user/login
/// -
/// - responseHeaders: [Set-Cookie(String), X-Rate-Limit(Int), X-Expires-After(Date)]
/// - parameter username: (query) The user name for login
/// - parameter password: (query) The password for login in clear text
/// - returns: AnyPublisher<String, Error>
open func loginUser(username: String, password: String) -> AnyPublisher<String, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
let path = "/user/login"
let url = baseURL.appendingPathComponent(path)
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
var queryItems: [URLQueryItem] = []
queryItems.append(URLQueryItem(name: "username", value: username))
queryItems.append(URLQueryItem(name: "password", value: password))
components?.queryItems = queryItems
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "GET"
return request
}.publisher
}.flatMap { request -> AnyPublisher<String, Error> in
return self.transport.send(request: request)
.mapError { transportError -> Error in
if transportError.statusCode == 400 {
return LoginUserError.code400Error
}
return transportError
}
.tryMap { response in
try self.decoder.decode(String.self, from: response.data)
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
/// Logs out current logged in user session
/// - GET /user/logout
/// -
/// - API Key:
/// - type: apiKey api_key
/// - name: api_key
/// - returns: AnyPublisher<Void, Error>
open func logoutUser() -> AnyPublisher<Void, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
let path = "/user/logout"
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "GET"
return request
}.publisher
}.flatMap { request -> AnyPublisher<Void, Error> in
return self.transport.send(request: request)
.tryMap { response in
return ()
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
public enum UpdateUserError: Error, CustomStringConvertible {
// Invalid user supplied
case code400Error
// User not found
case code404Error
public var description: String {
switch self {
case .code400Error:
return "UpdateUserError: Invalid user supplied"
case .code404Error:
return "UpdateUserError: User not found"
}
}
}
/// Updated user
/// - PUT /user/{username}
/// - This can only be done by the logged in user.
/// - API Key:
/// - type: apiKey api_key
/// - name: api_key
/// - parameter username: (path) name that need to be deleted
/// - parameter user: (body) Updated user object
/// - returns: AnyPublisher<Void, Error>
open func updateUser(username: String, user: User) -> AnyPublisher<Void, Error> {
Deferred {
Result<URLRequest, Error> {
guard let baseURL = self.transport.baseURL ?? self.baseURL else {
throw OpenAPITransportError.badURLError()
}
var path = "/user/{username}"
path = path.replacingOccurrences(of: "{username}", with: username)
let url = baseURL.appendingPathComponent(path)
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let requestURL = components?.url else {
throw OpenAPITransportError.badURLError()
}
var request = URLRequest(url: requestURL)
request.httpMethod = "PUT"
request.httpBody = try self.encoder.encode(user)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
return request
}.publisher
}.flatMap { request -> AnyPublisher<Void, Error> in
return self.transport.send(request: request)
.mapError { transportError -> Error in
if transportError.statusCode == 400 {
return UpdateUserError.code400Error
}
if transportError.statusCode == 404 {
return UpdateUserError.code404Error
}
return transportError
}
.tryMap { response in
return ()
}
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
}

View File

@ -0,0 +1,21 @@
//
// ApiResponse.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
//
import Foundation
/// Describes the result of uploading an image resource
public struct ApiResponse: Codable {
public var code: Int?
public var type: String?
public var message: String?
public init(code: Int? = nil, type: String? = nil, message: String? = nil) {
self.code = code
self.type = type
self.message = message
}
}

View File

@ -0,0 +1,19 @@
//
// Category.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
//
import Foundation
/// A category for a pet
public struct Category: Codable {
public var id: Int64?
public var name: String?
public init(id: Int64? = nil, name: String? = nil) {
self.id = id
self.name = name
}
}

View File

@ -0,0 +1,33 @@
//
// Order.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
//
import Foundation
/// An order for a pets from the pet store
public struct Order: Codable {
public enum Status: String, Codable, CaseIterable {
case placed = "placed"
case approved = "approved"
case delivered = "delivered"
}
public var id: Int64?
public var petId: Int64?
public var quantity: Int?
public var shipDate: Date?
/// Order Status
public var status: Status?
public var complete: Bool? = false
public init(id: Int64? = nil, petId: Int64? = nil, quantity: Int? = nil, shipDate: Date? = nil, status: Status? = nil, complete: Bool? = false) {
self.id = id
self.petId = petId
self.quantity = quantity
self.shipDate = shipDate
self.status = status
self.complete = complete
}
}

View File

@ -0,0 +1,33 @@
//
// Pet.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
//
import Foundation
/// A pet for sale in the pet store
public struct Pet: Codable {
public enum Status: String, Codable, CaseIterable {
case available = "available"
case pending = "pending"
case sold = "sold"
}
public var id: Int64?
public var category: Category?
public var name: String
public var photoUrls: [String]
public var tags: [Tag]?
/// pet status in the store
public var status: Status?
public init(id: Int64? = nil, category: Category? = nil, name: String, photoUrls: [String], tags: [Tag]? = nil, status: Status? = nil) {
self.id = id
self.category = category
self.name = name
self.photoUrls = photoUrls
self.tags = tags
self.status = status
}
}

View File

@ -0,0 +1,19 @@
//
// Tag.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
//
import Foundation
/// A tag for a pet
public struct Tag: Codable {
public var id: Int64?
public var name: String?
public init(id: Int64? = nil, name: String? = nil) {
self.id = id
self.name = name
}
}

View File

@ -0,0 +1,32 @@
//
// User.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
//
import Foundation
/// A User who is purchasing from the pet store
public struct User: Codable {
public var id: Int64?
public var username: String?
public var firstName: String?
public var lastName: String?
public var email: String?
public var password: String?
public var phone: String?
/// User Status
public var userStatus: Int?
public init(id: Int64? = nil, username: String? = nil, firstName: String? = nil, lastName: String? = nil, email: String? = nil, password: String? = nil, phone: String? = nil, userStatus: Int? = nil) {
self.id = id
self.username = username
self.firstName = firstName
self.lastName = lastName
self.email = email
self.password = password
self.phone = phone
self.userStatus = userStatus
}
}

View File

@ -0,0 +1,46 @@
//
// OpenISO8601DateFormatter.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
//
import Foundation
// https://stackoverflow.com/a/50281094/976628
class OpenISO8601DateFormatter: DateFormatter {
static let withoutSeconds: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
return formatter
}()
private func setup() {
calendar = Calendar(identifier: .iso8601)
locale = Locale(identifier: "en_US_POSIX")
timeZone = TimeZone(secondsFromGMT: 0)
dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
}
static var shared = OpenISO8601DateFormatter()
override init() {
super.init()
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override func date(from string: String) -> Date? {
if let result = super.date(from: string) {
return result
}
return OpenISO8601DateFormatter.withoutSeconds.date(from: string)
}
}

View File

@ -0,0 +1,7 @@
#!/bin/bash
set -e
DIRECTORY=`dirname $0`
(cd $DIRECTORY/tests ; xcodebuild -scheme TestClientTests-Package test -destination "platform=iOS Simulator,name=iPhone 14,OS=latest" | xcpretty && exit ${PIPESTATUS[0]})

View File

@ -0,0 +1,19 @@
// swift-tools-version:5.1
import PackageDescription
let package = Package(
name: "TestClientTests",
platforms: [
.iOS(.v13),
.macOS(.v10_15)
],
products: [],
dependencies: [.package(path: "../client/PetstoreOpenAPI")],
targets: [
.testTarget(
name: "TestClientTests",
dependencies: [.byName(name: "PetstoreOpenAPI")]
),
]
)

View File

@ -0,0 +1,118 @@
//
// PetAPITests.swift
//
//
// Created by Anton Davydov on 16.11.2021.
//
import XCTest
import Combine
import PetstoreOpenAPI
import OpenAPITransport
class PetAPITests: XCTestCase {
var cancellable = Set<AnyCancellable>()
let timeout: TimeInterval = 10
let baseURL = URL(string: "https://petstore.swagger.io/v2")!
override func tearDown() {
cancellable.removeAll()
}
func testAddPet() {
// Given
let transport = URLSessionOpenAPITransport(config: .init(baseURL: baseURL))
let api = PetAPI(transport)
let category = Category(id: 1, name: "CategoryName")
let photoUrls = ["https://petstore.com/sample/photo1.jpg", "https://petstore.com/sample/photo2.jpg"]
let tags = [Tag(id: 10, name: "Tag1"), Tag(id: 11, name: "Tag2")]
let pet = Pet(
id: 100,
category: category,
name: "PetName100",
photoUrls: photoUrls,
tags: tags,
status: .available
)
// When
let expectation = expectation(description: "addPetTestExpectation")
api.addPet(pet: pet)
.sink(receiveCompletion: { completion in
// Then
switch completion {
case .finished:
expectation.fulfill()
case let .failure(error):
XCTFail("Adding pet operation finished with error: \(error)")
expectation.fulfill()
}
}, receiveValue: { addedPet in
// Then
XCTAssertTrue(pet == addedPet, "Added pet should be the same as given value")
})
.store(in: &cancellable)
wait(for: [expectation], timeout: timeout)
}
func testGetPetByUnknownId() {
// Given
let transport = URLSessionOpenAPITransport(config: .init(baseURL: baseURL))
let api = PetAPI(transport)
let unknownPetId: Int64 = 1010101010
// When
let expectation = expectation(description: "testGetPetByIdExpectation")
api.getPetById(petId: unknownPetId)
.sink { completion in
switch completion {
case .finished:
XCTFail("Finding unknown pet operation should return 404 error")
case let .failure(error):
XCTAssertTrue((error as? PetAPI.GetPetByIdError) == .code404Error, "Finding unknown pet operation should return 404 error")
}
expectation.fulfill()
} receiveValue: { _ in }
.store(in: &cancellable)
wait(for: [expectation], timeout: timeout)
}
func testDeleteUnknownPet() {
// Given
let transport = URLSessionOpenAPITransport(config: .init(baseURL: baseURL))
let api = PetAPI(transport)
let unknownPetId: Int64 = 1010101010
// When
let expectation = expectation(description: "testDeletePetExpectation")
api
.deletePet(petId: unknownPetId, apiKey: "special-key")
.sink { completion in
// Then
switch completion {
case .finished:
XCTFail("Deleting unknown pet operation should return 404 error")
expectation.fulfill()
case let .failure(error):
XCTAssertTrue((error as? OpenAPITransportError)?.statusCode == 404, "Deleting unknown pet operation should return 404 error")
expectation.fulfill()
}
} receiveValue: {}
.store(in: &cancellable)
wait(for: [expectation], timeout: timeout)
}
}
extension Tag: Equatable {
public static func ==(l: Tag, r: Tag) -> Bool {
l.id == r.id && l.name == r.name
}
}
extension Pet: Equatable {
public static func ==(l: Pet, r: Pet) -> Bool {
l.id == r.id && l.name == r.name && l.photoUrls == r.photoUrls && l.status == r.status && l.tags == r.tags
}
}