Skip to content

Commit

Permalink
Use codegeneration for API layer on frontend (#2)
Browse files Browse the repository at this point in the history
* Fix openapi documentation

* cq

* Use codegeneration for API layer on frontend
  • Loading branch information
sonnymilton committed May 10, 2024
1 parent ec31766 commit 02dc1d5
Show file tree
Hide file tree
Showing 28 changed files with 1,778 additions and 205 deletions.
8 changes: 4 additions & 4 deletions backend/src/Controller/Api/RecipeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;

#[Route(path: '/recipe', name: 'recipe_')]
#[Route(path: '/recipe')]
#[OA\Tag('Recipe')]
final class RecipeController extends AbstractController
{
public function __construct(
private readonly RecipeFactory $factory,
private readonly RecipeRepository $repository, private readonly RecipeRepository $recipeRepository,
private readonly RecipeRepository $repository
) {
}

/** @return iterable<RecipeListItem> */
#[Route(path: '/', methods: ['GET'])]
#[OA\Response(response: 200, description: 'List recipes', content: new Model(type: RecipeListItem::class))]
#[OA\Response(response: 200, description: 'List recipes', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: new Model(type: RecipeListItem::class))))]
public function list(): iterable
{
return $this->repository->getRecipeList();
Expand All @@ -53,6 +53,6 @@ public function create(#[MapRequestPayload] RecipeDto $createRecipeDto): void
#[OA\Response(response: 404, description: 'Recipe not found')]
public function delete(string $vendor, string $packageName, string $version): void
{
$this->recipeRepository->delete($vendor, $packageName, $version);
$this->repository->delete($vendor, $packageName, $version);
}
}
2 changes: 1 addition & 1 deletion backend/src/Dto/Recipe/Manifest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
/**
* @param array<string, string> $copyFromRecipe
* @param array<string, string> $copyFromPackage
* @param array<string> $env
* @param array<string, string> $env
* @param array<string> $gitignore
* @param array<string, string> $composerScripts
*/
Expand Down
47 changes: 8 additions & 39 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,8 @@
# flex-server-front

This template should help get you started developing with Vue 3 in Vite.

## Recommended IDE Setup

[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).

## Type Support for `.vue` Imports in TS

TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.

## Customize configuration

See [Vite Configuration Reference](https://vitejs.dev/config/).

## Project Setup

```sh
npm install
```

### Compile and Hot-Reload for Development

```sh
npm run dev
```

### Type-Check, Compile and Minify for Production

```sh
npm run build
```

### Lint with [ESLint](https://eslint.org/)

```sh
npm run lint
```
# Flexhub frontend

## Code generation
[Flexhub.api.ts](src/Flexhub.api.ts) is generated with [sta](https://github.com/acacode/swagger-typescript-api).
Do not change anything inside the file.
Use
`npx sta -t flexhub-api-template -p http://nginx/api/doc.json -o ./src -n Flexhub.api.ts`
to generate new version of the file whenever you change API contracts on the backend.
69 changes: 69 additions & 0 deletions frontend/flexhub-api-template/api.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<%
const { apiConfig, routes, utils, config } = it;
const { info, servers, externalDocs } = apiConfig;
const { _, require, formatDescription } = utils;
const server = (servers && servers[0]) || { url: "" };
const descriptionLines = _.compact([
`@title ${info.title || "No title"}`,
info.version && `@version ${info.version}`,
info.license && `@license ${_.compact([
info.license.name,
info.license.url && `(${info.license.url})`,
]).join(" ")}`,
info.termsOfService && `@termsOfService ${info.termsOfService}`,
server.url && `@baseUrl ${server.url}`,
externalDocs.url && `@externalDocs ${externalDocs.url}`,
info.contact && `@contact ${_.compact([
info.contact.name,
info.contact.email && `<${info.contact.email}>`,
info.contact.url && `(${info.contact.url})`,
]).join(" ")}`,
info.description && " ",
info.description && _.replace(formatDescription(info.description), /\n/g, "\n * "),
]);
%>

<% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import type { AxiosRequestConfig, AxiosResponse } from "axios"; <% } %>

<% if (descriptionLines.length) { %>
/**
<% descriptionLines.forEach((descriptionLine) => { %>
* <%~ descriptionLine %>
<% }) %>
*/
<% } %>
export class <%~ config.apiClassName %><SecurityDataType extends unknown><% if (!config.singleHttpClient) { %> extends HttpClient<SecurityDataType> <% } %> {

<% if(config.singleHttpClient) { %>
http: HttpClient<SecurityDataType>;
constructor (http: HttpClient<SecurityDataType>) {
this.http = http;
}
<% } %>


<% if (routes.outOfModule) { %>
<% for (const route of routes.outOfModule) { %>
<%~ includeFile('./procedure-call.ejs', { ...it, route }) %>
<% } %>
<% } %>

<% if (routes.combined) { %>
<% for (const { routes: combinedRoutes = [], moduleName } of routes.combined) { %>
<%~ moduleName %> = {
<% for (const route of combinedRoutes) { %>
<%~ includeFile('./procedure-call.ejs', { ...it, route }) %>
<% } %>
}
<% } %>
<% } %>
}
37 changes: 37 additions & 0 deletions frontend/flexhub-api-template/data-contract-jsdoc.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<%
const { data, utils } = it;
const { formatDescription, require, _ } = utils;
const stringify = (value) => _.isObject(value) ? JSON.stringify(value) : _.isString(value) ? `"${value}"` : value
const jsDocLines = _.compact([
data.title,
data.description && formatDescription(data.description),
!_.isUndefined(data.deprecated) && data.deprecated && '@deprecated',
!_.isUndefined(data.format) && `@format ${data.format}`,
!_.isUndefined(data.minimum) && `@min ${data.minimum}`,
!_.isUndefined(data.multipleOf) && `@multipleOf ${data.multipleOf}`,
!_.isUndefined(data.exclusiveMinimum) && `@exclusiveMin ${data.exclusiveMinimum}`,
!_.isUndefined(data.maximum) && `@max ${data.maximum}`,
!_.isUndefined(data.minLength) && `@minLength ${data.minLength}`,
!_.isUndefined(data.maxLength) && `@maxLength ${data.maxLength}`,
!_.isUndefined(data.exclusiveMaximum) && `@exclusiveMax ${data.exclusiveMaximum}`,
!_.isUndefined(data.maxItems) && `@maxItems ${data.maxItems}`,
!_.isUndefined(data.minItems) && `@minItems ${data.minItems}`,
!_.isUndefined(data.uniqueItems) && `@uniqueItems ${data.uniqueItems}`,
!_.isUndefined(data.default) && `@default ${stringify(data.default)}`,
!_.isUndefined(data.pattern) && `@pattern ${data.pattern}`,
!_.isUndefined(data.example) && `@example ${stringify(data.example)}`
]).join('\n').split('\n');
%>
<% if (jsDocLines.every(_.isEmpty)) { %>
<% } else if (jsDocLines.length === 1) { %>
/** <%~ jsDocLines[0] %> */
<% } else if (jsDocLines.length) { %>
/**
<% for (jsDocLine of jsDocLines) { %>
* <%~ jsDocLine %>
<% } %>
*/
<% } %>
40 changes: 40 additions & 0 deletions frontend/flexhub-api-template/data-contracts.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<%
const { modelTypes, utils, config } = it;
const { formatDescription, require, _, Ts } = utils;
const buildGenerics = (contract) => {
if (!contract.genericArgs || !contract.genericArgs.length) return '';
return '<' + contract.genericArgs.map(({ name, default: defaultType, extends: extendsType }) => {
return [
name,
extendsType && `extends ${extendsType}`,
defaultType && `= ${defaultType}`,
].join('')
}).join(',') + '>'
}
const dataContractTemplates = {
enum: (contract) => {
return `enum ${contract.name} {\r\n${contract.content} \r\n }`;
},
interface: (contract) => {
return `interface ${contract.name}${buildGenerics(contract)} {\r\n${contract.content}}`;
},
type: (contract) => {
return `type ${contract.name}${buildGenerics(contract)} = ${contract.content}`;
},
}
%>

<% if (config.internalTemplateOptions.addUtilRequiredKeysType) { %>
type <%~ config.Ts.CodeGenKeyword.UtilRequiredKeys %><T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
<% } %>

<% for (const contract of modelTypes) { %>
<%~ includeFile('./data-contract-jsdoc.ejs', { ...it, data: { ...contract, ...contract.typeData } }) %>
<%~ contract.internal ? '' : 'export'%> <%~ (dataContractTemplates[contract.typeIdentifier] || dataContractTemplates.type)(contract) %>
<% } %>
12 changes: 12 additions & 0 deletions frontend/flexhub-api-template/enum-data-contract.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<%
const { contract, utils, config } = it;
const { formatDescription, require, _ } = utils;
const { name, $content } = contract;
%>
<% if (config.generateUnionEnums) { %>
export type <%~ name %> = <%~ _.map($content, ({ value }) => value).join(" | ") %>
<% } else { %>
export enum <%~ name %> {
<%~ _.map($content, ({ key, value }) => `${key} = ${value}`).join(",\n") %>
}
<% } %>
Loading

0 comments on commit 02dc1d5

Please sign in to comment.