Skip to content

Commit

Permalink
API Generator (#1000)
Browse files Browse the repository at this point in the history
* auto api generation.

* generator.

* fixes.

* refactor.

* added some fixes.

* tests for reader, refactor.

* refactor.

* added abstraction for reader.

* improved generated types.

* refactor.

* groups, abstract client.

* client implementation.

* refactor, checking for uniqueness, tests, renamed apt to aptos, advanced mapping.

* tests, fixes.

* first integration tests.

* integration tests.

* aptos API client.

* added missing dependencies.

* support for anyOf unions (simple types for now), refactor.

* virtual parameters, custom type determinants.

* test fixes.

* fixed reference.

* update sdk for updated swagger.

* complexity reduction.

* rename.

* simplified union/simple type files.

* merge fix.

* fixed export * from.

* fixed invalid import.

* some progress with mapping the swagger file for aptos API.

* update.

* add rollup.config to aptosApi package.

* rollup fix.

* new integration test.

* changeset.

* updated swagger + address normalization.

* fixed auth test.

* prettier fix.
  • Loading branch information
b4rtaz authored Mar 14, 2023
1 parent ea51fc8 commit ad49255
Show file tree
Hide file tree
Showing 269 changed files with 16,290 additions and 107 deletions.
5 changes: 5 additions & 0 deletions .changeset/light-singers-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@moralisweb3/common-aptos-utils': patch
---

The `AptosAddress` class normalizes an address to full length. By this is easier to compare two addresses.
5 changes: 5 additions & 0 deletions .changeset/strong-suits-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@moralisweb3/aptos-api': minor
---

Released the SDK for the Aptos API.
24 changes: 24 additions & 0 deletions packages/apiGenerator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# API Generator

This package provides a generator that can be used to generate a strong typed client based on a OpenAPI specification.

Supported versions of the OpenAPI specification:
* 3.0.x

Supported languages:
* TypeScript

## How to Run?

You need to call the generator by `ts-node` and provide the path to the folder with the configuration file. The configuration file is called `generator.config.json`.

```
ts-node src/index.ts ../../common/aptosUtils
```

## Architecture

The generator is split into two parts:

* The OpenAPI reader - it's responsible for reading the OpenAPI specification and creating normalized data structures that can be used by the generator. That data structure is called a contract. This architecture allows us to support multiple versions of the OpenAPI specification. Additionally, the data structure of the contract is designed for easy generating the object-oriented structure.
* The generator - it's responsible for generating the code based on the contract data structure.
8 changes: 8 additions & 0 deletions packages/apiGenerator/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
preset: 'ts-jest/presets/default',
testMatch: ['**/?(*.)+(spec|test).ts?(x)'],
transform: {
'^.+\\.(ts|js)x?$': 'ts-jest',
},
transformIgnorePatterns: ['^.+\\.js$'],
};
18 changes: 18 additions & 0 deletions packages/apiGenerator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@moralisweb3/api-generator",
"private": true,
"scripts": {
"gen:aptos": "ts-node src/index.ts ../../common/aptosUtils",
"gen:playground": "ts-node src/index.ts ../playground",
"test": "jest",
"test:watch": "jest --watch"
},
"devDependencies": {
"@types/jest": "^29.2.6",
"axios": "^1.2.2",
"jest": "29.3.1",
"openapi-types": "^12.1.0",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
}
}
1 change: 1 addition & 0 deletions packages/apiGenerator/playground/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
generated/
22 changes: 22 additions & 0 deletions packages/apiGenerator/playground/generator.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"url": "https://deep-index.moralis.io/api-docs-2.1/v2.1/swagger.json",
"generator": {
"outputDir": "generated",
"classNamePrefix": "Ply",
"mappings": {
"types": [],
"refs": [],
"complexTypeProperties": [],
"operationParameters": []
},
"typeDeterminants": []
},
"openApiReader": {
"v3": {
"operations": {
"groupRef": "#/x-tag-sdk",
"isEnabledRef": "#/x-tag-sdk"
}
}
}
}
8 changes: 8 additions & 0 deletions packages/apiGenerator/src/configuration/Configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { OpenApiReaderConfiguration } from 'src/reader/OpenApiReaderConfiguration';
import { GeneratorConfiguration } from '../generator/GeneratorConfiguration';

export interface Configuration {
url: string;
generator: GeneratorConfiguration;
openApiReader: OpenApiReaderConfiguration;
}
14 changes: 14 additions & 0 deletions packages/apiGenerator/src/configuration/ConfigurationReader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import path from 'path';
import fs from 'fs';
import { Configuration } from './Configuration';

export class ConfigurationReader {
public static read(projectPath: string): Configuration {
const jsonPath = path.join(projectPath, 'generator.config.json');
if (!fs.existsSync(jsonPath)) {
throw new Error(`Cannot open ${jsonPath} file`);
}
const jsonRaw = fs.readFileSync(jsonPath, 'utf-8');
return JSON.parse(jsonRaw) as Configuration;
}
}
109 changes: 109 additions & 0 deletions packages/apiGenerator/src/generator/Generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { IndexFileGenerator } from './fileGenerators/IndexFileGenerator';
import { OperationFileGenerator } from './fileGenerators/OperationFileGenerator';
import { SimpleTypeFileGenerator } from './fileGenerators/SimpleTypeFileGenerator';
import { ComplexTypeFileGenerator } from './fileGenerators/ComplexTypeFileGenerator';
import { GeneratorWriter } from './GeneratorWriter';
import {
ComplexTypeInfo,
OpenApiContract,
OperationInfo,
SimpleTypeInfo,
UnionTypeInfo,
} from '../reader/OpenApiContract';
import { TypeResolver } from './fileGenerators/resolvers/TypeResolver';
import { AbstractClientFileGenerator } from './fileGenerators/AbstractClientFileGenerator';
import { MappingResolver } from './fileGenerators/resolvers/MappingResolver';
import { UnionTypeFileGenerator } from './fileGenerators/UnionTypeFileGenerator';
import { GeneratorConfiguration } from './GeneratorConfiguration';
import { TypeDeterminantResolver } from './fileGenerators/resolvers/TypeDeterminantResolver';
import { TypeInfoResolver } from './fileGenerators/resolvers/TypeInfoResolver';

export class Generator {
public static create(
contract: OpenApiContract,
configuration: GeneratorConfiguration,
projectPath: string,
): Generator {
const mappingResolver = new MappingResolver(configuration.mappings);
const typeInfoResolver = new TypeInfoResolver(contract);
const typeResolver = new TypeResolver(configuration.classNamePrefix, mappingResolver, typeInfoResolver);
const typeDeterminantResolver = new TypeDeterminantResolver(configuration.typeDeterminants);
const generatorWriter = new GeneratorWriter(projectPath, configuration.outputDir);
return new Generator(contract, typeResolver, typeDeterminantResolver, typeInfoResolver, generatorWriter);
}

private readonly typesIndexGenerator = new IndexFileGenerator();
private readonly operationsIndexGenerator = new IndexFileGenerator();

private constructor(
private readonly contract: OpenApiContract,
private readonly typeResolver: TypeResolver,
private readonly typeDeterminantResolver: TypeDeterminantResolver,
private readonly typeInfoResolver: TypeInfoResolver,
private readonly writer: GeneratorWriter,
) {}

public generate() {
this.writer.prepare();

for (const operation of this.contract.operations) {
this.generateOperation(operation);
}
for (const simpleType of this.contract.simpleTypes) {
this.generateSimpleType(simpleType);
}
for (const complexType of this.contract.complexTypes) {
this.generateComplexType(complexType);
}
for (const unionType of this.contract.unionTypes) {
this.generateUnionType(unionType);
}

const abstractClientFileGenerator = new AbstractClientFileGenerator(this.contract.operations, this.typeResolver);
this.writer.writeAbstractClient(abstractClientFileGenerator.generate());

this.writer.writeTypesIndex(this.typesIndexGenerator.generate());
this.writer.writeOperationsIndex(this.operationsIndexGenerator.generate());
}

private generateOperation(info: OperationInfo) {
const generator = new OperationFileGenerator(info, this.typeResolver);
const result = generator.generate();

this.writer.writeOperation(result.className, result.output);

this.operationsIndexGenerator.add(result.className);
}

private generateSimpleType(info: SimpleTypeInfo) {
const generator = new SimpleTypeFileGenerator(info, this.typeResolver);
const result = generator.generate();

this.writer.writeType(result.className, result.output);

this.typesIndexGenerator.add(result.className);
}

private generateComplexType(info: ComplexTypeInfo) {
const generator = new ComplexTypeFileGenerator(
info,
this.typeResolver,
this.typeDeterminantResolver,
this.typeInfoResolver,
);
const result = generator.generate();

this.writer.writeType(result.className, result.output);

this.typesIndexGenerator.add(result.className);
}

private generateUnionType(info: UnionTypeInfo) {
const generator = new UnionTypeFileGenerator(info, this.typeResolver);
const result = generator.generate();

this.writer.writeType(result.className, result.output);

this.typesIndexGenerator.add(result.className);
}
}
40 changes: 40 additions & 0 deletions packages/apiGenerator/src/generator/GeneratorConfiguration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export interface GeneratorConfiguration {
outputDir: string;
classNamePrefix: string;
mappings: Mappings;
typeDeterminants: TypeDeterminant[];
}

export interface Mappings {
types: TypeMapping[];
refs: RefMapping[];
complexTypeProperties: ComplexTypePropertyMapping[];
operationParameters: OperationParameterMapping[];
}

export interface MappingTarget {
className?: string;
import?: string;
}

export interface TypeMapping extends MappingTarget {
typeName: string;
}

export interface RefMapping extends MappingTarget {
$ref: string;
}

export interface ComplexTypePropertyMapping extends MappingTarget {
names: string[];
}

export interface OperationParameterMapping extends MappingTarget {
names: string[];
}

export interface TypeDeterminant {
typeName: string;
isInputCode: string;
isJSONCode: string;
}
57 changes: 57 additions & 0 deletions packages/apiGenerator/src/generator/GeneratorWriter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import path from 'path';
import fs from 'fs';
import { Output } from './output/Output';

export class GeneratorWriter {
private readonly outputPath: string;
private readonly typesPath: string;
private readonly operationsPath: string;
private readonly clientPath: string;

public constructor(projectPath: string, outputDir: string) {
this.outputPath = path.join(projectPath, outputDir);
this.typesPath = path.join(this.outputPath, 'types');
this.operationsPath = path.join(this.outputPath, 'operations');
this.clientPath = path.join(this.outputPath, 'client');
}

private prepareDir(path: string) {
fs.mkdirSync(path, {
recursive: true,
});
fs.readdirSync(path).forEach((fileName) => {
fs.rmSync(`${path}/${fileName}`);
});
}

public prepare() {
this.prepareDir(this.typesPath);
this.prepareDir(this.operationsPath);
this.prepareDir(this.clientPath);
}

public writeOperation(className: string, output: Output) {
this.writeFile(this.operationsPath, className + '.ts', output);
}

public writeOperationsIndex(output: Output) {
this.writeFile(this.operationsPath, 'index.ts', output);
}

public writeType(className: string, output: Output) {
this.writeFile(this.typesPath, className + '.ts', output);
}

public writeTypesIndex(output: Output) {
this.writeFile(this.typesPath, 'index.ts', output);
}

public writeAbstractClient(output: Output) {
this.writeFile(this.clientPath, 'abstractClient.ts', output);
}

private writeFile(basePath: string, fileName: string, output: Output) {
const finalPath = path.join(basePath, fileName);
fs.writeFileSync(finalPath, output.toString(), 'utf-8');
}
}
Loading

1 comment on commit ad49255

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage

Title Lines Statements Branches Functions
api-utils Coverage: 30%
30.34% (61/201) 30.35% (17/56) 30.76% (12/39)
auth Coverage: 90%
92.59% (100/108) 83.33% (20/24) 86.2% (25/29)
evm-api Coverage: 100%
100% (82/82) 66.66% (6/9) 100% (50/50)
common-aptos-utils Coverage: 4%
4.81% (147/3054) 5.47% (25/457) 5.54% (44/793)
common-evm-utils Coverage: 64%
64.59% (987/1528) 20.02% (134/669) 35.21% (212/602)
sol-api Coverage: 96%
96.66% (29/30) 66.66% (6/9) 91.66% (11/12)
common-sol-utils Coverage: 74%
74.55% (167/224) 66.66% (18/27) 65.38% (51/78)
common-streams-utils Coverage: 91%
91.58% (1176/1284) 78.83% (406/515) 82.45% (423/513)
streams Coverage: 88%
87.81% (555/632) 65.88% (56/85) 87.59% (120/137)

Please sign in to comment.