forked from loafle/openapi-generator-original
add ts inversify generator
This commit is contained in:
parent
ffa89dc373
commit
4887be0b1e
@ -17,7 +17,7 @@ if [ ! -d "${APP_DIR}" ]; then
|
||||
APP_DIR=`cd "${APP_DIR}"; pwd`
|
||||
fi
|
||||
|
||||
executable="./modules/swagger-codegen-cli/target/swagger-codegen-cli.jar"
|
||||
executable="./modules/openapi-generator-cli/target/openapi-generator-cli.jar"
|
||||
|
||||
if [ ! -f "$executable" ]
|
||||
then
|
||||
@ -26,6 +26,6 @@ fi
|
||||
|
||||
# if you've executed sbt assembly previously it will use that instead.
|
||||
export JAVA_OPTS="${JAVA_OPTS} -XX:MaxPermSize=256M -Xmx1024M -DloggerPath=conf/log4j.properties"
|
||||
ags="$@ generate -i modules/swagger-codegen/src/test/resources/2_0/petstore.yaml -l typescript-inversify -o samples/client/petstore/typescript-inversify"
|
||||
ags="$@ generate -i modules/openapi-generator/src/test/resources/2_0/petstore.yaml -l typescript-inversify -o samples/client/petstore/typescript-inversify"
|
||||
|
||||
java $JAVA_OPTS -jar $executable $ags
|
||||
|
@ -0,0 +1,374 @@
|
||||
package org.openapitools.codegen.languages;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import io.swagger.v3.parser.util.SchemaTypeUtil;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.openapitools.codegen.*;
|
||||
import org.openapitools.codegen.languages.features.BeanValidationFeatures;
|
||||
import org.openapitools.codegen.languages.features.JbossFeature;
|
||||
import org.openapitools.codegen.languages.features.SwaggerFeatures;
|
||||
import io.swagger.v3.oas.models.*;
|
||||
import io.swagger.v3.oas.models.media.*;
|
||||
|
||||
|
||||
public class TypeScriptInversifyClientCodegen extends AbstractTypeScriptClientCodegen {
|
||||
private static final SimpleDateFormat SNAPSHOT_SUFFIX_FORMAT = new SimpleDateFormat("yyyyMMddHHmm");
|
||||
private static final String X_DISCRIMINATOR_TYPE = "x-discriminator-value";
|
||||
|
||||
public static final String NPM_NAME = "npmName";
|
||||
public static final String NPM_VERSION = "npmVersion";
|
||||
public static final String NPM_REPOSITORY = "npmRepository";
|
||||
public static final String SNAPSHOT = "snapshot";
|
||||
public static final String WITH_INTERFACES = "withInterfaces";
|
||||
public static final String USE_PROMISE = "usePromise";
|
||||
public static final String TAGGED_UNIONS = "taggedUnions";
|
||||
|
||||
protected String npmVersion = null;
|
||||
protected String npmName = null;
|
||||
protected String npmRepository = null;
|
||||
private boolean taggedUnions = false;
|
||||
|
||||
public TypeScriptInversifyClientCodegen() {
|
||||
super();
|
||||
this.outputFolder = "generated-code/typescript-inversify";
|
||||
|
||||
embeddedTemplateDir = templateDir = "typescript-inversify";
|
||||
modelTemplateFiles.put("model.mustache", ".ts");
|
||||
apiTemplateFiles.put("api.service.mustache", ".ts");
|
||||
languageSpecificPrimitives.add("Blob");
|
||||
typeMapping.put("file", "Blob");
|
||||
apiPackage = "api";
|
||||
modelPackage = "model";
|
||||
|
||||
this.cliOptions.add(new CliOption(NPM_NAME, "The name under which you want to publish generated npm package"));
|
||||
this.cliOptions.add(new CliOption(NPM_VERSION, "The version of your npm package"));
|
||||
this.cliOptions.add(new CliOption(NPM_REPOSITORY,
|
||||
"Use this property to set an url your private npmRepo in the package.json"));
|
||||
this.cliOptions.add(new CliOption(SNAPSHOT,
|
||||
"When setting this property to true the version will be suffixed with -SNAPSHOT.yyyyMMddHHmm",
|
||||
SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString()));
|
||||
this.cliOptions.add(new CliOption(WITH_INTERFACES,
|
||||
"Setting this property to true will generate interfaces next to the default class implementations.",
|
||||
SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString()));
|
||||
this.cliOptions.add(new CliOption(USE_PROMISE,
|
||||
"Setting this property to use promise instead of observable inside every service.",
|
||||
SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString()));
|
||||
this.cliOptions.add(new CliOption(TAGGED_UNIONS,
|
||||
"Use discriminators to create tagged unions instead of extending interfaces.",
|
||||
SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAdditionPropertiesToCodeGenModel(CodegenModel codegenModel, Schema schema) {
|
||||
codegenModel.additionalPropertiesType = getTypeDeclaration((Schema) schema.getAdditionalProperties());
|
||||
addImport(codegenModel, codegenModel.additionalPropertiesType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "typescript-inversify";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelp() {
|
||||
return "Generates Typescript services using Inversify IOC";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processOpts() {
|
||||
super.processOpts();
|
||||
// HttpClient
|
||||
supportingFiles.add(new SupportingFile("IHttpClient.mustache", getIndexDirectory(), "IHttpClient.ts"));
|
||||
supportingFiles.add(new SupportingFile("IAPIConfiguration.mustache", getIndexDirectory(), "IAPIConfiguration.ts"));
|
||||
supportingFiles.add(new SupportingFile("HttpClient.mustache", getIndexDirectory(), "HttpClient.ts"));
|
||||
supportingFiles.add(new SupportingFile("HttpResponse.mustache", getIndexDirectory(), "HttpResponse.ts"));
|
||||
supportingFiles.add(new SupportingFile("Headers.mustache", getIndexDirectory(), "Headers.ts"));
|
||||
|
||||
supportingFiles.add(new SupportingFile("ApiServiceBinder.mustache", getIndexDirectory(), "ApiServiceBinder.ts"));
|
||||
supportingFiles.add(new SupportingFile("variables.mustache", getIndexDirectory(), "variables.ts"));
|
||||
|
||||
if (additionalProperties.containsKey(NPM_NAME)) {
|
||||
addNpmPackageGeneration();
|
||||
}
|
||||
|
||||
if (additionalProperties.containsKey(WITH_INTERFACES)) {
|
||||
boolean withInterfaces = Boolean.parseBoolean(additionalProperties.get(WITH_INTERFACES).toString());
|
||||
if (withInterfaces) {
|
||||
apiTemplateFiles.put("apiInterface.mustache", "Interface.ts");
|
||||
}
|
||||
}
|
||||
|
||||
if (additionalProperties.containsKey(TAGGED_UNIONS)) {
|
||||
taggedUnions = Boolean.parseBoolean(additionalProperties.get(TAGGED_UNIONS).toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void addNpmPackageGeneration() {
|
||||
if (additionalProperties.containsKey(NPM_NAME)) {
|
||||
this.setNpmName(additionalProperties.get(NPM_NAME).toString());
|
||||
}
|
||||
|
||||
if (additionalProperties.containsKey(NPM_VERSION)) {
|
||||
this.setNpmVersion(additionalProperties.get(NPM_VERSION).toString());
|
||||
}
|
||||
|
||||
if (additionalProperties.containsKey(SNAPSHOT)
|
||||
&& Boolean.valueOf(additionalProperties.get(SNAPSHOT).toString())) {
|
||||
this.setNpmVersion(npmVersion + "-SNAPSHOT." + SNAPSHOT_SUFFIX_FORMAT.format(new Date()));
|
||||
}
|
||||
additionalProperties.put(NPM_VERSION, npmVersion);
|
||||
|
||||
if (additionalProperties.containsKey(NPM_REPOSITORY)) {
|
||||
this.setNpmRepository(additionalProperties.get(NPM_REPOSITORY).toString());
|
||||
}
|
||||
|
||||
//Files for building our lib
|
||||
supportingFiles.add(new SupportingFile("models.mustache", modelPackage().replace('.', File.separatorChar), "models.ts"));
|
||||
supportingFiles.add(new SupportingFile("apis.mustache", apiPackage().replace('.', File.separatorChar), "api.ts"));
|
||||
supportingFiles.add(new SupportingFile("index.mustache", getIndexDirectory(), "index.ts"));
|
||||
supportingFiles.add(new SupportingFile("gitignore", "", ".gitignore"));
|
||||
supportingFiles.add(new SupportingFile("git_push.sh.mustache", "", "git_push.sh"));
|
||||
supportingFiles.add(new SupportingFile("README.mustache", getIndexDirectory(), "README.md"));
|
||||
supportingFiles.add(new SupportingFile("package.mustache", getIndexDirectory(), "package.json"));
|
||||
supportingFiles.add(new SupportingFile("tsconfig.mustache", getIndexDirectory(), "tsconfig.json"));
|
||||
}
|
||||
|
||||
private String getIndexDirectory() {
|
||||
String indexPackage = modelPackage.substring(0, Math.max(0, modelPackage.lastIndexOf('.')));
|
||||
return indexPackage.replace('.', File.separatorChar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDataTypeFile(final String dataType) {
|
||||
return dataType != null && dataType.equals("Blob");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeDeclaration(Schema p) {
|
||||
if (p instanceof FileSchema) {
|
||||
return "Blob";
|
||||
} else if (!StringUtils.isEmpty(p.get$ref())) {
|
||||
return "any";
|
||||
} else {
|
||||
return super.getTypeDeclaration(p);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getSchemaType(Schema p) {
|
||||
String swaggerType = super.getSchemaType(p);
|
||||
if (isLanguagePrimitive(swaggerType) || isLanguageGenericType(swaggerType)) {
|
||||
return swaggerType;
|
||||
}
|
||||
applyLocalTypeMapping(swaggerType);
|
||||
return swaggerType;
|
||||
}
|
||||
|
||||
private String applyLocalTypeMapping(String type) {
|
||||
if (typeMapping.containsKey(type)) {
|
||||
type = typeMapping.get(type);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private boolean isLanguagePrimitive(String type) {
|
||||
return languageSpecificPrimitives.contains(type);
|
||||
}
|
||||
|
||||
private boolean isLanguageGenericType(String type) {
|
||||
for (String genericType : languageGenericTypes) {
|
||||
if (type.startsWith(genericType + "<")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessParameter(CodegenParameter parameter) {
|
||||
super.postProcessParameter(parameter);
|
||||
parameter.dataType = applyLocalTypeMapping(parameter.dataType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> postProcessOperations(Map<String, Object> operations) {
|
||||
Map<String, Object> objs = (Map<String, Object>) operations.get("operations");
|
||||
|
||||
// Add filename information for api imports
|
||||
objs.put("apiFilename", getApiFilenameFromClassname(objs.get("classname").toString()));
|
||||
|
||||
List<CodegenOperation> ops = (List<CodegenOperation>) objs.get("operation");
|
||||
for (CodegenOperation op : ops) {
|
||||
// Prep a string buffer where we're going to set up our new version of the string.
|
||||
StringBuilder pathBuffer = new StringBuilder();
|
||||
StringBuilder parameterName = new StringBuilder();
|
||||
int insideCurly = 0;
|
||||
|
||||
op.httpMethod = op.httpMethod.toLowerCase();
|
||||
|
||||
// Iterate through existing string, one character at a time.
|
||||
for (int i = 0; i < op.path.length(); i++) {
|
||||
switch (op.path.charAt(i)) {
|
||||
case '{':
|
||||
// We entered curly braces, so track that.
|
||||
insideCurly++;
|
||||
|
||||
// Add the more complicated component instead of just the brace.
|
||||
pathBuffer.append("${encodeURIComponent(String(");
|
||||
break;
|
||||
case '}':
|
||||
// We exited curly braces, so track that.
|
||||
insideCurly--;
|
||||
|
||||
// Add the more complicated component instead of just the brace.
|
||||
pathBuffer.append(toVarName(parameterName.toString()));
|
||||
pathBuffer.append("))}");
|
||||
parameterName.setLength(0);
|
||||
break;
|
||||
default:
|
||||
if (insideCurly > 0) {
|
||||
parameterName.append(op.path.charAt(i));
|
||||
} else {
|
||||
pathBuffer.append(op.path.charAt(i));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Overwrite path to TypeScript template string, after applying everything we just did.
|
||||
op.path = pathBuffer.toString();
|
||||
}
|
||||
|
||||
// Add additional filename information for model imports in the services
|
||||
List<Map<String, Object>> imports = (List<Map<String, Object>>) operations.get("imports");
|
||||
for (Map<String, Object> im : imports) {
|
||||
im.put("filename", im.get("import"));
|
||||
im.put("classname", getModelnameFromModelFilename(im.get("filename").toString()));
|
||||
}
|
||||
|
||||
return operations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> postProcessModels(Map<String, Object> objs) {
|
||||
Map<String, Object> result = super.postProcessModels(objs);
|
||||
|
||||
return postProcessModelsEnum(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> postProcessAllModels(Map<String, Object> objs) {
|
||||
Map<String, Object> result = super.postProcessAllModels(objs);
|
||||
|
||||
for (Map.Entry<String, Object> entry : result.entrySet()) {
|
||||
Map<String, Object> inner = (Map<String, Object>) entry.getValue();
|
||||
List<Map<String, Object>> models = (List<Map<String, Object>>) inner.get("models");
|
||||
for (Map<String, Object> mo : models) {
|
||||
CodegenModel cm = (CodegenModel) mo.get("model");
|
||||
if (taggedUnions) {
|
||||
mo.put(TAGGED_UNIONS, true);
|
||||
if (cm.discriminator != null && cm.children != null) {
|
||||
for (CodegenModel child : cm.children) {
|
||||
cm.imports.add(child.classname);
|
||||
}
|
||||
}
|
||||
if (cm.parent != null) {
|
||||
cm.imports.remove(cm.parent);
|
||||
}
|
||||
}
|
||||
// Add additional filename information for imports
|
||||
mo.put("tsImports", toTsImports(cm, cm.imports));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<Map<String, String>> toTsImports(CodegenModel cm, Set<String> imports) {
|
||||
List<Map<String, String>> tsImports = new ArrayList<>();
|
||||
for (String im : imports) {
|
||||
if (!im.equals(cm.classname)) {
|
||||
HashMap<String, String> tsImport = new HashMap<>();
|
||||
tsImport.put("classname", im);
|
||||
tsImport.put("filename", toModelFilename(im));
|
||||
tsImports.add(tsImport);
|
||||
}
|
||||
}
|
||||
return tsImports;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toApiName(String name) {
|
||||
if (name.length() == 0) {
|
||||
return "DefaultService";
|
||||
}
|
||||
return initialCaps(name) + "Service";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toApiFilename(String name) {
|
||||
if (name.length() == 0) {
|
||||
return "default.service";
|
||||
}
|
||||
return camelize(name, true) + ".service";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toApiImport(String name) {
|
||||
return apiPackage() + "/" + toApiFilename(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toModelFilename(String name) {
|
||||
return camelize(toModelName(name), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toModelImport(String name) {
|
||||
return modelPackage() + "/" + toModelFilename(name);
|
||||
}
|
||||
|
||||
public String getNpmName() {
|
||||
return npmName;
|
||||
}
|
||||
|
||||
public void setNpmName(String npmName) {
|
||||
this.npmName = npmName;
|
||||
}
|
||||
|
||||
public String getNpmVersion() {
|
||||
return npmVersion;
|
||||
}
|
||||
|
||||
public void setNpmVersion(String npmVersion) {
|
||||
this.npmVersion = npmVersion;
|
||||
}
|
||||
|
||||
public String getNpmRepository() {
|
||||
return npmRepository;
|
||||
}
|
||||
|
||||
public void setNpmRepository(String npmRepository) {
|
||||
this.npmRepository = npmRepository;
|
||||
}
|
||||
|
||||
private String getApiFilenameFromClassname(String classname) {
|
||||
String name = classname.substring(0, classname.length() - "Service".length());
|
||||
return toApiFilename(name);
|
||||
}
|
||||
|
||||
private String getModelnameFromModelFilename(String filename) {
|
||||
String name = filename.substring((modelPackage() + "/").length());
|
||||
return camelize(name);
|
||||
}
|
||||
|
||||
}
|
@ -80,5 +80,6 @@ org.openapitools.codegen.languages.TypeScriptAngularClientCodegen
|
||||
org.openapitools.codegen.languages.TypeScriptAngularJsClientCodegen
|
||||
org.openapitools.codegen.languages.TypeScriptAureliaClientCodegen
|
||||
org.openapitools.codegen.languages.TypeScriptFetchClientCodegen
|
||||
org.openapitools.codegen.languages.TypeScriptInversifyClientCodegen
|
||||
org.openapitools.codegen.languages.TypeScriptJqueryClientCodegen
|
||||
org.openapitools.codegen.languages.TypeScriptNodeClientCodegen
|
||||
|
@ -0,0 +1,20 @@
|
||||
import {interfaces} from "inversify";
|
||||
|
||||
{{#apiInfo}}
|
||||
{{#apis}}
|
||||
import { {{classname}} } from './{{importPath}}';
|
||||
{{#withInterfaces}}
|
||||
import { {{classname}}Interface } from './{{importPath}}Interface';
|
||||
{{/withInterfaces}}
|
||||
{{/apis}}
|
||||
{{/apiInfo}}
|
||||
|
||||
export class ApiServiceBinder {
|
||||
public static with(container: interfaces.Container) {
|
||||
{{#apiInfo}}
|
||||
{{#apis}}
|
||||
container.bind<{{classname}}{{#withInterfaces}}Interface{{/withInterfaces}}>("{{classname}}").to({{classname}}).inSingletonScope();
|
||||
{{/apis}}
|
||||
{{/apiInfo}}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export interface Headers {
|
||||
[index:string]: string
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import IHttpClient from "./IHttpClient";
|
||||
import { Observable } from "rxjs/Observable";
|
||||
import "whatwg-fetch";
|
||||
import HttpResponse from "./HttpResponse";
|
||||
import {injectable} from "inversify";
|
||||
import "rxjs/add/observable/fromPromise";
|
||||
import { Headers } from "./Headers";
|
||||
|
||||
@injectable()
|
||||
class HttpClient implements IHttpClient {
|
||||
|
||||
get(url:string, headers?: Headers):Observable<HttpResponse> {
|
||||
return this.performNetworkCall(url, "get", undefined, headers);
|
||||
}
|
||||
|
||||
post(url: string, body: {}|FormData, headers?: Headers): Observable<HttpResponse> {
|
||||
return this.performNetworkCall(url, "post", this.getJsonBody(body), this.addJsonHeaders(headers));
|
||||
}
|
||||
|
||||
put(url: string, body: {}, headers?: Headers): Observable<HttpResponse> {
|
||||
return this.performNetworkCall(url, "put", this.getJsonBody(body), this.addJsonHeaders(headers));
|
||||
}
|
||||
|
||||
delete(url: string, headers?: Headers): Observable<HttpResponse> {
|
||||
return this.performNetworkCall(url, "delete", undefined, headers);
|
||||
}
|
||||
|
||||
private getJsonBody(body: {}|FormData) {
|
||||
return !(body instanceof FormData) ? JSON.stringify(body) : body;
|
||||
}
|
||||
|
||||
private addJsonHeaders(headers: Headers) {
|
||||
return Object.assign({}, {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}, headers);
|
||||
};
|
||||
|
||||
private performNetworkCall(url: string, method: string, body?: any, headers?: Headers): Observable<HttpResponse> {
|
||||
let promise = window.fetch(url, {
|
||||
method: method,
|
||||
body: body,
|
||||
headers: <any>headers
|
||||
}).then(response => {
|
||||
let headers: Headers = {};
|
||||
response.headers.forEach((value, name) => {
|
||||
headers[name.toString().toLowerCase()] = value;
|
||||
});
|
||||
return response.text().then(text => {
|
||||
let contentType = headers["content-type"] || "";
|
||||
let payload = contentType.match("application/json") ? JSON.parse(text) : text;
|
||||
let httpResponse = new HttpResponse(payload, response.status, headers);
|
||||
|
||||
if (response.status >= 400)
|
||||
throw httpResponse;
|
||||
return httpResponse;
|
||||
});
|
||||
});
|
||||
return Observable.fromPromise(promise);
|
||||
}
|
||||
}
|
||||
|
||||
export default HttpClient
|
@ -0,0 +1,8 @@
|
||||
import { Headers } from "./Headers"
|
||||
|
||||
class HttpResponse<T = any> {
|
||||
constructor(public response: T, public status:number, public headers?: Headers) {
|
||||
}
|
||||
}
|
||||
|
||||
export default HttpResponse
|
@ -0,0 +1,8 @@
|
||||
export interface IAPIConfiguration {
|
||||
apiKeys?: {[ key: string ]: string};
|
||||
username?: string;
|
||||
password?: string;
|
||||
accessToken?: string | (() => string);
|
||||
basePath?: string;
|
||||
withCredentials?: boolean;
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { Observable } from "rxjs/Observable";
|
||||
import HttpResponse from "./HttpResponse";
|
||||
import { Headers } from "./Headers";
|
||||
|
||||
interface IHttpClient {
|
||||
get(url:string, headers?: Headers):Observable<HttpResponse>
|
||||
post(url:string, body:{}|FormData, headers?: Headers):Observable<HttpResponse>
|
||||
put(url:string, body:{}, headers?: Headers):Observable<HttpResponse>
|
||||
delete(url:string, headers?: Headers):Observable<HttpResponse>
|
||||
}
|
||||
|
||||
export default IHttpClient
|
@ -0,0 +1,74 @@
|
||||
## {{npmName}}@{{npmVersion}}
|
||||
|
||||
### Building
|
||||
|
||||
To build an compile the typescript sources to javascript use:
|
||||
```
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
### publishing
|
||||
|
||||
First build the package than run ```npm publish```
|
||||
|
||||
### consuming
|
||||
|
||||
navigate to the folder of your consuming project and run one of next commando's.
|
||||
|
||||
_published:_
|
||||
|
||||
```
|
||||
npm install {{npmName}}@{{npmVersion}} --save
|
||||
```
|
||||
|
||||
_unPublished (not recommended):_
|
||||
|
||||
```
|
||||
npm install PATH_TO_GENERATED_PACKAGE --save
|
||||
```
|
||||
|
||||
_using `npm link`:_
|
||||
|
||||
In PATH_TO_GENERATED_PACKAGE:
|
||||
```
|
||||
npm link
|
||||
```
|
||||
|
||||
In your project:
|
||||
```
|
||||
npm link {{npmName}}@{{npmVersion}}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
Services require a `IHttpClient` and a `IApiConfiguration`. The `IHttpClient` is necessary to manage http's call and with the `IApiConfiguration` you can provide settings for the Authentication.
|
||||
For the sake of simplicity an implementation of `IHttpClient` is already provided, but if you want you can override it.
|
||||
For these reasons you have to manually bind these two services:
|
||||
|
||||
```typescript
|
||||
let container = new Container();
|
||||
container.bind<IHttpClient>("IApiHttpClient").to(HttpClient).inSingletonScope();
|
||||
container.bind<IApiConfiguration>("IApiConfiguration").to(ApiConfiguration).inSingletonScope();
|
||||
```
|
||||
|
||||
|
||||
## Services Binding
|
||||
To bind all the generated services you can use `ApiServiceBinder`.
|
||||
|
||||
```typescript
|
||||
ApiServiceBinder.with(container);
|
||||
```
|
||||
|
||||
## Final result
|
||||
|
||||
```typescript
|
||||
let container = new Container();
|
||||
container.bind<IHttpClient>("IApiHttpClient").to(HttpClient).inSingletonScope();
|
||||
container.bind<IApiConfiguration>("IApiConfiguration").to(ApiConfiguration).inSingletonScope();
|
||||
ApiServiceBinder.with(container);
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,180 @@
|
||||
{{>licenseInfo}}
|
||||
/* tslint:disable:no-unused-variable member-ordering */
|
||||
|
||||
import { Observable } from "rxjs/Observable";
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
import IHttpClient from "../IHttpClient";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { IAPIConfiguration } from "../IAPIConfiguration";
|
||||
import { Headers } from "../Headers";
|
||||
import HttpResponse from "../HttpResponse";
|
||||
|
||||
{{#imports}}
|
||||
import { {{classname}} } from '../{{filename}}';
|
||||
{{/imports}}
|
||||
|
||||
import { COLLECTION_FORMATS } from '../variables';
|
||||
{{#withInterfaces}}
|
||||
import { {{classname}}Interface } from './{{classname}}Interface';
|
||||
{{/withInterfaces}}
|
||||
|
||||
{{#operations}}
|
||||
|
||||
{{#description}}
|
||||
/**
|
||||
* {{&description}}
|
||||
*/
|
||||
{{/description}}
|
||||
|
||||
@injectable()
|
||||
{{#withInterfaces}}
|
||||
export class {{classname}} implements {{classname}}Interface {
|
||||
{{/withInterfaces}}
|
||||
{{^withInterfaces}}
|
||||
export class {{classname}} {
|
||||
{{/withInterfaces}}
|
||||
private basePath: string = '{{{basePath}}}';
|
||||
|
||||
constructor(@inject("IApiHttpClient") private httpClient: IHttpClient,
|
||||
@inject("IAPIConfiguration") private APIConfiguration: IAPIConfiguration ) {
|
||||
if(this.APIConfiguration.basePath)
|
||||
this.basePath = this.APIConfiguration.basePath;
|
||||
}
|
||||
{{#operation}}
|
||||
|
||||
/**
|
||||
* {{summary}}
|
||||
* {{notes}}
|
||||
{{#allParams}}* @param {{paramName}} {{description}}
|
||||
{{/allParams}}{{#useHttpClient}}* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
|
||||
* @param reportProgress flag to report request and response progress.{{/useHttpClient}}
|
||||
*/
|
||||
public {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}observe?: 'body', headers?: Headers): {{#usePromise}}Promise{{/usePromise}}{{^usePromise}}Observable{{/usePromise}}<{{#returnType}}{{{returnType}}}{{#isResponseTypeFile}}|undefined{{/isResponseTypeFile}}{{/returnType}}{{^returnType}}any{{/returnType}}>;
|
||||
public {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}observe?: 'response', headers?: Headers): {{#usePromise}}Promise{{/usePromise}}{{^usePromise}}Observable{{/usePromise}}<HttpResponse<{{#returnType}}{{{returnType}}}{{#isResponseTypeFile}}|undefined{{/isResponseTypeFile}}{{/returnType}}{{^returnType}}any{{/returnType}}>>;
|
||||
public {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}observe: any = 'body', headers: Headers = {}): {{#usePromise}}Promise{{/usePromise}}{{^usePromise}}Observable{{/usePromise}}<any> {
|
||||
{{#allParams}}
|
||||
{{#required}}
|
||||
if (!{{paramName}}){
|
||||
throw new Error('Required parameter {{paramName}} was null or undefined when calling {{nickname}}.');
|
||||
}
|
||||
|
||||
{{/required}}
|
||||
{{/allParams}}
|
||||
{{#hasQueryParams}}
|
||||
let queryParameters: string[] = [];
|
||||
{{#queryParams}}
|
||||
{{#isListContainer}}
|
||||
if ({{paramName}}) {
|
||||
{{#isCollectionFormatMulti}}
|
||||
{{paramName}}.forEach((element) => {
|
||||
queryParameters.push("{{paramName}}="+encodeURIComponent(String({{paramName}})));
|
||||
})
|
||||
{{/isCollectionFormatMulti}}
|
||||
{{^isCollectionFormatMulti}}
|
||||
queryParameters.push("{{paramName}}="+encodeURIComponent({{paramName}}.join(COLLECTION_FORMATS['{{collectionFormat}}'])));
|
||||
{{/isCollectionFormatMulti}}
|
||||
}
|
||||
{{/isListContainer}}
|
||||
{{^isListContainer}}
|
||||
if ({{paramName}} !== undefined) {
|
||||
{{#isDateTime}}
|
||||
queryParameters.push("{{paramName}}="+encodeURIComponent(<any>{{paramName}}.toISOString()));
|
||||
{{/isDateTime}}
|
||||
{{^isDateTime}}
|
||||
queryParameters.push("{{paramName}}="+encodeURIComponent(String({{paramName}})));
|
||||
{{/isDateTime}}
|
||||
}
|
||||
{{/isListContainer}}
|
||||
{{/queryParams}}
|
||||
|
||||
{{/hasQueryParams}}
|
||||
{{#headerParams}}
|
||||
{{#isListContainer}}
|
||||
if ({{paramName}}) {
|
||||
headers['{{baseName}}'] = {{paramName}}.join(COLLECTION_FORMATS['{{collectionFormat}}']);
|
||||
}
|
||||
{{/isListContainer}}
|
||||
{{^isListContainer}}
|
||||
if ({{paramName}}) {
|
||||
headers['{{baseName}}'] = String({{paramName}});
|
||||
}
|
||||
{{/isListContainer}}
|
||||
|
||||
{{/headerParams}}
|
||||
{{#authMethods}}
|
||||
// authentication ({{name}}) required
|
||||
{{#isApiKey}}
|
||||
{{#isKeyInHeader}}
|
||||
if (this.APIConfiguration.apiKeys["{{keyParamName}}"]) {
|
||||
headers['{{keyParamName}}'] = this.APIConfiguration.apiKeys["{{keyParamName}}"];
|
||||
}
|
||||
{{/isKeyInHeader}}
|
||||
{{#isKeyInQuery}}
|
||||
if (this.APIConfiguration.apiKeys["{{keyParamName}}"]) {
|
||||
queryParameters.push("{{paramName}}="+encodeURIComponent(String(this.APIConfiguration.apiKeys["{{keyParamName}}"])));
|
||||
}
|
||||
{{/isKeyInQuery}}
|
||||
{{/isApiKey}}
|
||||
{{#isBasic}}
|
||||
if (this.APIConfiguration.username || this.APIConfiguration.password) {
|
||||
headers['Authorization'] = btoa(this.APIConfiguration.username + ':' + this.APIConfiguration.password);
|
||||
}
|
||||
{{/isBasic}}
|
||||
{{#isOAuth}}
|
||||
if (this.APIConfiguration.accessToken) {
|
||||
let accessToken = typeof this.APIConfiguration.accessToken === 'function'
|
||||
? this.APIConfiguration.accessToken()
|
||||
: this.APIConfiguration.accessToken;
|
||||
headers['Authorization'] = 'Bearer ' + accessToken;
|
||||
}
|
||||
{{/isOAuth}}
|
||||
{{/authMethods}}
|
||||
{{^produces}}
|
||||
headers['Accept'] = 'application/json';
|
||||
{{/produces}}
|
||||
{{#produces.0}}
|
||||
headers['Accept'] = '{{{mediaType}}}';
|
||||
{{/produces.0}}
|
||||
{{#bodyParam}}
|
||||
{{^consumes}}
|
||||
headers['Content-Type'] = 'application/json';
|
||||
{{/consumes}}
|
||||
{{#consumes.0}}
|
||||
headers['Content-Type'] = '{{{mediaType}}}';
|
||||
{{/consumes.0}}
|
||||
{{/bodyParam}}
|
||||
|
||||
{{#hasFormParams}}
|
||||
let formData: FormData = new FormData();
|
||||
headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
|
||||
{{#formParams}}
|
||||
{{#isListContainer}}
|
||||
if ({{paramName}}) {
|
||||
{{#isCollectionFormatMulti}}
|
||||
{{paramName}}.forEach((element) => {
|
||||
formData.append('{{baseName}}', <any>element);
|
||||
})
|
||||
{{/isCollectionFormatMulti}}
|
||||
{{^isCollectionFormatMulti}}
|
||||
formData.append('{{baseName}}', {{paramName}}.join(COLLECTION_FORMATS['{{collectionFormat}}']));
|
||||
{{/isCollectionFormatMulti}}
|
||||
}
|
||||
{{/isListContainer}}
|
||||
{{^isListContainer}}
|
||||
if ({{paramName}} !== undefined) {
|
||||
formData.append('{{baseName}}', <any>{{paramName}});
|
||||
}
|
||||
{{/isListContainer}}
|
||||
{{/formParams}}
|
||||
|
||||
{{/hasFormParams}}
|
||||
const response: Observable<HttpResponse<{{#returnType}}{{{returnType}}}{{#isResponseTypeFile}}|undefined{{/isResponseTypeFile}}{{/returnType}}{{^returnType}}any{{/returnType}}>> = this.httpClient.{{httpMethod}}(`${this.basePath}{{{path}}}{{#hasQueryParams}}?${queryParameters.join('&')}{{/hasQueryParams}}`{{#bodyParam}}, {{paramName}} {{/bodyParam}}{{#hasFormParams}}, body{{/hasFormParams}}, headers);
|
||||
if (observe == 'body') {
|
||||
return response.map(httpResponse => <{{#returnType}}{{{returnType}}}{{#isResponseTypeFile}}|undefined{{/isResponseTypeFile}}{{/returnType}}{{^returnType}}any{{/returnType}}>(httpResponse.response)){{#usePromise}}.toPromise(){{/usePromise}};
|
||||
}
|
||||
return response{{#usePromise}}.toPromise(){{/usePromise}};
|
||||
}
|
||||
|
||||
{{/operation}}}
|
||||
{{/operations}}
|
@ -0,0 +1,26 @@
|
||||
{{>licenseInfo}}
|
||||
import { Headers } from "../Headers";
|
||||
import { Observable } from "rxjs/Observable";
|
||||
import * as models from "../model/models";
|
||||
import HttpResponse from "../HttpResponse";
|
||||
|
||||
{{#operations}}
|
||||
|
||||
{{#description}}
|
||||
/**
|
||||
* {{&description}}
|
||||
*/
|
||||
{{/description}}
|
||||
export interface {{classname}}Interface {
|
||||
{{#operation}}
|
||||
/**
|
||||
* {{summary}}
|
||||
* {{notes}}
|
||||
{{#allParams}}* @param {{paramName}} {{description}}
|
||||
{{/allParams}}*/
|
||||
|
||||
{{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}observe?: 'body', headers?: Headers): {{#usePromise}}Promise{{/usePromise}}{{^usePromise}}Observable{{/usePromise}}<{{#returnType}}{{{returnType}}}{{#isResponseTypeFile}}|undefined{{/isResponseTypeFile}}{{/returnType}}{{^returnType}}any{{/returnType}}>;
|
||||
{{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}observe?: 'response', headers?: Headers): {{#usePromise}}Promise{{/usePromise}}{{^usePromise}}Observable{{/usePromise}}<HttpResponse<{{#returnType}}{{{returnType}}}{{#isResponseTypeFile}}|undefined{{/isResponseTypeFile}}{{/returnType}}{{^returnType}}any{{/returnType}}>>;
|
||||
{{/operation}}
|
||||
}
|
||||
{{/operations}}
|
@ -0,0 +1,5 @@
|
||||
{{#apiInfo}}
|
||||
{{#apis}}
|
||||
export * from './{{ classFilename }}';
|
||||
{{/apis}}
|
||||
{{/apiInfo}}
|
@ -0,0 +1,52 @@
|
||||
#!/bin/sh
|
||||
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
|
||||
#
|
||||
# Usage example: /bin/sh ./git_push.sh wing328 swagger-petstore-perl "minor update"
|
||||
|
||||
git_user_id=$1
|
||||
git_repo_id=$2
|
||||
release_note=$3
|
||||
|
||||
if [ "$git_user_id" = "" ]; then
|
||||
git_user_id="{{{gitUserId}}}"
|
||||
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
|
||||
fi
|
||||
|
||||
if [ "$git_repo_id" = "" ]; then
|
||||
git_repo_id="{{{gitRepoId}}}"
|
||||
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
|
||||
fi
|
||||
|
||||
if [ "$release_note" = "" ]; then
|
||||
release_note="{{{releaseNote}}}"
|
||||
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
|
||||
fi
|
||||
|
||||
# Initialize the local directory as a Git repository
|
||||
git init
|
||||
|
||||
# Adds the files in the local repository and stages them for commit.
|
||||
git add .
|
||||
|
||||
# Commits the tracked changes and prepares them to be pushed to a remote repository.
|
||||
git commit -m "$release_note"
|
||||
|
||||
# Sets the new remote
|
||||
git_remote=`git remote`
|
||||
if [ "$git_remote" = "" ]; then # git remote not defined
|
||||
|
||||
if [ "$GIT_TOKEN" = "" ]; then
|
||||
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
|
||||
git remote add origin https://github.com/${git_user_id}/${git_repo_id}.git
|
||||
else
|
||||
git remote add origin https://${git_user_id}:${GIT_TOKEN}@github.com/${git_user_id}/${git_repo_id}.git
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
git pull origin master
|
||||
|
||||
# Pushes (Forces) the changes in the local repository up to the remote repository
|
||||
echo "Git pushing to https://github.com/${git_user_id}/${git_repo_id}.git"
|
||||
git push origin master 2>&1 | grep -v 'To https'
|
||||
|
@ -0,0 +1,6 @@
|
||||
wwwroot/*.js
|
||||
node_modules
|
||||
typings
|
||||
dist
|
||||
.vscode
|
||||
.idea
|
@ -0,0 +1,7 @@
|
||||
export * from './api/api';
|
||||
export * from './model/models';
|
||||
export * from './variables';
|
||||
export * from './IAPIConfiguration';
|
||||
export * from './ApiServiceBinder';
|
||||
export * from './IHttpClient';
|
||||
export * from './HttpClient';
|
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* {{{appName}}}
|
||||
* {{{appDescription}}}
|
||||
*
|
||||
* {{#version}}OpenAPI spec version: {{{version}}}{{/version}}
|
||||
* {{#infoEmail}}Contact: {{{infoEmail}}}{{/infoEmail}}
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
@ -0,0 +1,16 @@
|
||||
{{>licenseInfo}}
|
||||
{{#models}}
|
||||
{{#model}}
|
||||
{{#tsImports}}
|
||||
import { {{classname}} } from './{{filename}}';
|
||||
{{/tsImports}}
|
||||
|
||||
|
||||
{{#description}}
|
||||
/**
|
||||
* {{{description}}}
|
||||
*/
|
||||
{{/description}}
|
||||
{{#isEnum}}{{>modelEnum}}{{/isEnum}}{{^isEnum}}{{#isAlias}}{{>modelAlias}}{{/isAlias}}{{^isAlias}}{{#taggedUnions}}{{>modelTaggedUnion}}{{/taggedUnions}}{{^taggedUnions}}{{>modelGeneric}}{{/taggedUnions}}{{/isAlias}}{{/isEnum}}
|
||||
{{/model}}
|
||||
{{/models}}
|
@ -0,0 +1 @@
|
||||
export type {{classname}} = {{dataType}};
|
@ -0,0 +1,9 @@
|
||||
export type {{classname}} = {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}} | {{/-last}}{{/enumVars}}{{/allowableValues}};
|
||||
|
||||
export const {{classname}} = {
|
||||
{{#allowableValues}}
|
||||
{{#enumVars}}
|
||||
{{name}}: {{{value}}} as {{classname}}{{^-last}},{{/-last}}
|
||||
{{/enumVars}}
|
||||
{{/allowableValues}}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
export interface {{classname}} {{#parent}}extends {{{parent}}} {{/parent}}{ {{>modelGenericAdditionalProperties}}
|
||||
{{#vars}}
|
||||
{{#description}}
|
||||
/**
|
||||
* {{{description}}}
|
||||
*/
|
||||
{{/description}}
|
||||
{{#isReadOnly}}readonly {{/isReadOnly}}{{name}}{{^required}}?{{/required}}: {{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{datatype}}}{{/isEnum}};
|
||||
{{/vars}}
|
||||
}{{>modelGenericEnums}}
|
@ -0,0 +1,5 @@
|
||||
{{#additionalPropertiesType}}
|
||||
|
||||
[key: string]: {{{additionalPropertiesType}}}{{#hasVars}} | any{{/hasVars}};
|
||||
|
||||
{{/additionalPropertiesType}}
|
@ -0,0 +1,16 @@
|
||||
{{#hasEnums}}
|
||||
|
||||
export namespace {{classname}} {
|
||||
{{#vars}}
|
||||
{{#isEnum}}
|
||||
export type {{enumName}} = {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}} | {{/-last}}{{/enumVars}}{{/allowableValues}};
|
||||
export const {{enumName}} = {
|
||||
{{#allowableValues}}
|
||||
{{#enumVars}}
|
||||
{{name}}: {{{value}}} as {{enumName}}{{^-last}},{{/-last}}
|
||||
{{/enumVars}}
|
||||
{{/allowableValues}}
|
||||
}
|
||||
{{/isEnum}}
|
||||
{{/vars}}
|
||||
}{{/hasEnums}}
|
@ -0,0 +1,21 @@
|
||||
{{#discriminator}}
|
||||
export type {{classname}} = {{#children}}{{^-first}} | {{/-first}}{{classname}}{{/children}};
|
||||
{{/discriminator}}
|
||||
{{^discriminator}}
|
||||
{{#parent}}
|
||||
export interface {{classname}} { {{>modelGenericAdditionalProperties}}
|
||||
{{#allVars}}
|
||||
{{#description}}
|
||||
/**
|
||||
* {{{description}}}
|
||||
*/
|
||||
{{/description}}
|
||||
{{name}}{{^required}}?{{/required}}: {{#discriminatorValue}}'{{discriminatorValue}}'{{/discriminatorValue}}{{^discriminatorValue}}{{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{datatype}}}{{/isEnum}}{{/discriminatorValue}};
|
||||
{{/allVars}}
|
||||
}
|
||||
{{>modelGenericEnums}}
|
||||
{{/parent}}
|
||||
{{^parent}}
|
||||
{{>modelGeneric}}
|
||||
{{/parent}}
|
||||
{{/discriminator}}
|
@ -0,0 +1,5 @@
|
||||
{{#models}}
|
||||
{{#model}}
|
||||
export * from './{{{ classFilename }}}';
|
||||
{{/model}}
|
||||
{{/models}}
|
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "{{{npmName}}}",
|
||||
"version": "{{{npmVersion}}}",
|
||||
"description": "swagger client for {{{npmName}}}",
|
||||
"author": "Swagger Codegen Contributors",
|
||||
"keywords": [
|
||||
"swagger-client"
|
||||
],
|
||||
"license": "Unlicense",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --outDir dist/",
|
||||
"postinstall": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"inversify": "^4.3.0",
|
||||
"rxjs": "~5.5.7",
|
||||
"whatwg-fetch": "~2.0.1",
|
||||
"reflect-metadata": "0.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
}{{#npmRepository}},{{/npmRepository}}
|
||||
{{#npmRepository}}
|
||||
"publishConfig": {
|
||||
"registry": "{{{npmRepository}}}"
|
||||
}
|
||||
{{/npmRepository}}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"noImplicitAny": false,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"target": "{{#supportsES6}}es6{{/supportsES6}}{{^supportsES6}}es5{{/supportsES6}}",
|
||||
"module": "{{#supportsES6}}es6{{/supportsES6}}{{^supportsES6}}commonjs{{/supportsES6}}",
|
||||
"moduleResolution": "node",
|
||||
"removeComments": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"noLib": false,
|
||||
"declaration": true,
|
||||
"lib": [ "es6", "dom" ]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
],
|
||||
"filesGlob": [
|
||||
"./model/*.ts",
|
||||
"./api/*.ts"
|
||||
]
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
export const COLLECTION_FORMATS = {
|
||||
'csv': ',',
|
||||
'tsv': ' ',
|
||||
'ssv': ' ',
|
||||
'pipes': '|'
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user