Compare commits

..

3 Commits

Author SHA1 Message Date
Vlad
46698078a0 fix generator api parameters 2021-03-09 18:56:53 +03:00
Vlad
59a3045615 add api generator 2021-03-01 13:32:27 +03:00
Vlad
1453c27d87 wip: Update entities generator 2021-02-25 17:40:38 +03:00
22 changed files with 717 additions and 728 deletions

View File

@@ -13,32 +13,19 @@ and this project adheres to
## [v0.106.0] - 2021-04-26 ## [v0.106.0] - 2021-04-26
--> -->
## [v0.105.2] - 2021-03-10 <!--
## [v0.105.2] - 2021-02-24
-->
### Fixed ### Fixed
- Incomplete hostnames with trailing zero-bytes handling ([#2582]).
- Wrong DNS-over-TLS ALPN configuration ([#2681]).
- Inconsistent responses for messages with EDNS0 and AD when DNS caching is
enabled ([#2600]).
- Incomplete OpenWRT detection ([#2757]).
- DHCP lease's `expired` field incorrect time format ([#2692]). - DHCP lease's `expired` field incorrect time format ([#2692]).
- Incomplete DNS upstreams validation ([#2674]). - Incomplete DNS upstreams validation ([#2674]).
- Wrong parsing of DHCP options of the `ip` type ([#2688]). - Wrong parsing of DHCP options of the `ip` type ([#2688]).
[#2582]: https://github.com/AdguardTeam/AdGuardHome/issues/2582
[#2600]: https://github.com/AdguardTeam/AdGuardHome/issues/2600
[#2674]: https://github.com/AdguardTeam/AdGuardHome/issues/2674 [#2674]: https://github.com/AdguardTeam/AdGuardHome/issues/2674
[#2681]: https://github.com/AdguardTeam/AdGuardHome/issues/2681
[#2688]: https://github.com/AdguardTeam/AdGuardHome/issues/2688 [#2688]: https://github.com/AdguardTeam/AdGuardHome/issues/2688
[#2692]: https://github.com/AdguardTeam/AdGuardHome/issues/2692 [#2692]: https://github.com/AdguardTeam/AdGuardHome/issues/2692
[#2757]: https://github.com/AdguardTeam/AdGuardHome/issues/2757
### Security
- Session token doesn't contain user's information anymore ([#2470]).
[#2470]: https://github.com/AdguardTeam/AdGuardHome/issues/2470
@@ -223,12 +210,11 @@ and this project adheres to
<!-- <!--
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.106.0...HEAD
[v0.106.0]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.2...v0.106.0
-->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.2...HEAD [Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.2...HEAD
[v0.105.2]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.1...v0.105.2 [v0.105.2]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.1...v0.105.2
-->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.1...HEAD
[v0.105.1]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.0...v0.105.1 [v0.105.1]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.0...v0.105.1
[v0.105.0]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.3...v0.105.0 [v0.105.0]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.3...v0.105.0
[v0.104.3]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.2...v0.104.3 [v0.104.3]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.2...v0.104.3

View File

@@ -3,7 +3,7 @@ import React, {
useEffect, useEffect,
useRef, useRef,
} from 'react'; } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import propTypes from 'prop-types'; import propTypes from 'prop-types';
import throttle from 'lodash/throttle'; import throttle from 'lodash/throttle';
@@ -25,21 +25,19 @@ const InfiniteTable = ({
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const loader = useRef(null); const loader = useRef(null);
const loadingRef = useRef(null);
const isEntireLog = useSelector((state) => state.queryLogs.isEntireLog); const {
const processingGetLogs = useSelector((state) => state.queryLogs.processingGetLogs); isEntireLog,
processingGetLogs,
} = useSelector((state) => state.queryLogs, shallowEqual);
const loading = isLoading || processingGetLogs; const loading = isLoading || processingGetLogs;
const listener = useCallback(() => { const listener = useCallback(() => {
if (!loadingRef.current && loader.current && isScrolledIntoView(loader.current)) { if (loader.current && isScrolledIntoView(loader.current)) {
dispatch(getLogs()); dispatch(getLogs());
} }
}, []); }, [loader.current, isScrolledIntoView, getLogs]);
useEffect(() => {
loadingRef.current = processingGetLogs;
}, [processingGetLogs]);
useEffect(() => { useEffect(() => {
listener(); listener();

View File

@@ -76,7 +76,7 @@
"stylelint-webpack-plugin": "^2.1.1", "stylelint-webpack-plugin": "^2.1.1",
"terser-webpack-plugin": "^5.0.0", "terser-webpack-plugin": "^5.0.0",
"ts-loader": "^8.0.6", "ts-loader": "^8.0.6",
"ts-morph": "^8.1.2", "ts-morph": "^10.0.1",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",
"typescript": "^4.0.3", "typescript": "^4.0.3",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",

View File

@@ -9,4 +9,5 @@ export const trimQuotes = (str: string) => {
return str.replace(/\'|\"/g, ''); return str.replace(/\'|\"/g, '');
}; };
export const GENERATOR_ENTITY_ALLIAS = 'Entities/'; export const GENERATOR_ENTITY_ALLIAS = 'Entities/';
export const BAD_REQUES_HELPER = 'BadRequesHelper';

View File

@@ -4,9 +4,10 @@ import { OPEN_API_PATH } from '../consts';
import EntitiesGenerator from './src/generateEntities'; import EntitiesGenerator from './src/generateEntities';
import ApisGenerator from './src/generateApis'; import ApisGenerator from './src/generateApis';
import { OpenApi } from './src/utils';
const generateApi = (openApi: Record<string, any>) => { const generateApi = (openApi: OpenApi) => {
const ent = new EntitiesGenerator(openApi); const ent = new EntitiesGenerator(openApi);
ent.save(); ent.save();
@@ -14,5 +15,5 @@ const generateApi = (openApi: Record<string, any>) => {
api.save(); api.save();
} }
const openApiFile = fs.readFileSync(OPEN_API_PATH, 'utf8'); const openApiFile = fs.readFileSync('./scripts/generator/v1.yaml', 'utf8');
generateApi(YAML.parse(openApiFile)); generateApi(YAML.parse(openApiFile));

View File

@@ -2,15 +2,16 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */ /* eslint-disable @typescript-eslint/no-unused-expressions */
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { stringify } from 'qs'; import { number } from 'prop-types';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import * as morph from 'ts-morph'; import * as morph from 'ts-morph';
import { import {
API_DIR as API_DIR_CONST, API_DIR as API_DIR_CONST,
BAD_REQUES_HELPER,
GENERATOR_ENTITY_ALLIAS, GENERATOR_ENTITY_ALLIAS,
} from '../../consts'; } from '../../consts';
import { toCamel, capitalize, schemaParamParser } from './utils'; import { toCamel, capitalize, schemaParamParser, OpenApi, uncapitalize, RequestBody } from './utils';
const API_DIR = path.resolve(API_DIR_CONST); const API_DIR = path.resolve(API_DIR_CONST);
@@ -20,11 +21,15 @@ if (!fs.existsSync(API_DIR)) {
const { Project, QuoteKind } = morph; const { Project, QuoteKind } = morph;
enum PROCESS_AS {
JSON = 'JSON',
TEXT = 'TEXT',
EMPTY = 'EMPTY',
}
class ApiGenerator { class ApiGenerator {
project = new Project({ project = new Project({
tsConfigFilePath: './tsconfig.json', tsConfigFilePath: './tsconfig.json',
addFilesFromTsConfig: false,
manipulationSettings: { manipulationSettings: {
quoteKind: QuoteKind.Single, quoteKind: QuoteKind.Single,
usePrefixAndSuffixTextForRename: false, usePrefixAndSuffixTextForRename: false,
@@ -32,7 +37,7 @@ class ApiGenerator {
}, },
}); });
openapi: Record<string, any>; openapi: OpenApi;
serverUrl: string; serverUrl: string;
@@ -47,20 +52,28 @@ class ApiGenerator {
apis: morph.SourceFile[] = []; apis: morph.SourceFile[] = [];
constructor(openapi: Record<string, any>) { methods = ['patch', 'delete', 'post', 'get', 'put', 'head', 'options', 'trace'];
constructor(openapi: OpenApi) {
this.openapi = openapi; this.openapi = openapi;
this.paths = openapi.paths; this.paths = openapi.paths;
this.serverUrl = openapi.servers[0].url; this.serverUrl = openapi.servers[0].url;
Object.keys(this.paths).forEach((pathKey) => { Object.keys(this.paths).forEach((pathKey) => {
Object.keys(this.paths[pathKey]).forEach((method) => { Object.keys(this.paths[pathKey]).filter((method) => this.methods.includes(method)).forEach((method) => {
const { const {
tags, operationId, parameters, responses, requestBody, security, tags, operationId, responses, requestBody, security, "x-skip-web-api": skip
} = this.paths[pathKey][method]; } = this.paths[pathKey][method];
const controller = toCamel((tags ? tags[0] : pathKey.split('/')[1]).replace('-controller', '')); const parameters = this.paths[pathKey][method].parameters || this.paths[pathKey].parameters;
const controller = toCamel((tags ? tags[0] : pathKey.split('/')[1]));
if (skip) {
return;
}
if (!operationId) {
console.log(pathKey);
}
if (this.controllers[controller]) { if (this.controllers[controller]) {
this.controllers[controller][operationId] = { this.controllers[controller][uncapitalize(operationId)] = {
parameters, parameters,
responses, responses,
method, method,
@@ -69,7 +82,7 @@ class ApiGenerator {
pathKey: pathKey.replace(/{/g, '${'), pathKey: pathKey.replace(/{/g, '${'),
}; };
} else { } else {
this.controllers[controller] = { [operationId]: { this.controllers[controller] = { [uncapitalize(operationId)]: {
parameters, parameters,
responses, responses,
method, method,
@@ -97,7 +110,7 @@ class ApiGenerator {
]); ]);
// const schemaProperties = schemas[schemaName].properties; // const schemaProperties = schemas[schemaName].properties;
const importEntities: any[] = []; const importEntities: { type: string, isClass: boolean }[] = [];
// add api class to file // add api class to file
const apiClass = apiFile.addClass({ const apiClass = apiFile.addClass({
@@ -111,29 +124,34 @@ class ApiGenerator {
// for each operation add fetcher // for each operation add fetcher
operationList.forEach((operation) => { operationList.forEach((operation) => {
const { const {
requestBody, responses, parameters, method, pathKey, security, requestBody, responses, parameters, method, pathKey,
} = controllerOperations[operation]; } = controllerOperations[operation];
const queryParams: any[] = []; // { name, type } const queryParams: { name: string, type: string, hasQuestionToken: boolean }[] = [];
const bodyParam: any[] = []; // { name, type } const bodyParam: { name: string, countedType: string, type?: string, isClass?: boolean, hasQuestionToken: boolean }[] = [];
let contentType: string = '';
let hasResponseBodyType: /* boolean | ReturnType<schemaParamParser> */ false | [string, boolean, boolean, boolean, boolean] = false;
let contentType = '';
if (parameters) { if (parameters) {
parameters.forEach((p: any) => { parameters.forEach((link: {$ref: string}) => {
const [ const temp = link.$ref.split('/').pop()
pType, isArray, isClass, isImport, const parameter = this.openapi.components.parameters[temp!];
] = schemaParamParser(p.schema, this.openapi);
const {
type, isArray, isClass, isImport,
} = schemaParamParser(parameter.schema, this.openapi);
if (isImport) { if (isImport) {
importEntities.push({ type: pType, isClass }); importEntities.push({ type, isClass });
} }
if (p.in === 'query') { if (parameter.in === 'query') {
queryParams.push({ queryParams.push({
name: p.name, type: `${pType}${isArray ? '[]' : ''}`, hasQuestionToken: !p.required }); name: parameter.name, type: `${type}${isArray ? '[]' : ''}`, hasQuestionToken: !parameter.required });
} }
}); });
} }
if (queryParams.length > 0) { if (queryParams.length > 0) {
const imp = apiFile.getImportDeclaration((i) => { const imp = apiFile.getImportDeclaration((i) => {
return i.getModuleSpecifierValue() === 'qs'; return i.getModuleSpecifierValue() === 'qs';
@@ -144,62 +162,120 @@ class ApiGenerator {
}); });
} }
} }
if (requestBody) { if (requestBody) {
let content = requestBody.content;
const { $ref }: { $ref: string } = requestBody; const { $ref }: { $ref: string } = requestBody;
if (!content && $ref) { const name = $ref.split('/').pop();
const name = $ref.split('/').pop() as string; const { content, required } = this.openapi.components.requestBodies[name!];
content = this.openapi.components.requestBodies[name].content;
}
[contentType] = Object.keys(content); [contentType] = Object.keys(content);
const data = content[contentType]; const data = content[contentType as keyof RequestBody['content']]!;
const [ const {
pType, isArray, isClass, isImport, type, isArray, isClass, isImport,
] = schemaParamParser(data.schema, this.openapi); } = schemaParamParser(data.schema, this.openapi);
if (isImport) { if (isImport) {
importEntities.push({ type: pType, isClass }); importEntities.push({ type: type, isClass });
bodyParam.push({ name: pType.toLowerCase(), type: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`, isClass, pType }); bodyParam.push({
name: type.toLowerCase(),
countedType: `${isClass ? 'I' : ''}${type}${isArray ? '[]' : ''}`,
isClass,
type,
hasQuestionToken: !required
});
} else { } else {
bodyParam.push({ name: 'data', type: `${pType}${isArray ? '[]' : ''}` }); bodyParam.push({
name: 'data',
countedType: `${type}${isArray ? '[]' : ''}`,
hasQuestionToken: !required });
} }
} }
if (responses['200']) {
const { content, headers } = responses['200'];
if (content && (content['*/*'] || content['application/json'])) {
const { schema, examples } = content['*/*'] || content['application/json'];
if (!schema) { const responsesCodes = Object.keys(responses);
process.exit(0); const responsesSchema = responsesCodes.map((code) => {
const refLink = responses[code].$ref.split('/').pop();
const ref = this.openapi.components.responses[refLink];
interface ResponseSchema {
code: number,
[PROCESS_AS.JSON]?: ReturnType<typeof schemaParamParser>;
[PROCESS_AS.TEXT]?: {
schema?: ReturnType<typeof schemaParamParser>;
xErrorCode?: string;
onlyText: boolean;
} }
[PROCESS_AS.EMPTY]?: boolean;
const propType = schemaParamParser(schema, this.openapi);
const [pType, , isClass, isImport] = propType;
if (isImport) {
importEntities.push({ type: pType, isClass });
}
hasResponseBodyType = propType;
} }
} const responseSchema: ResponseSchema = { code: Number(code) };
let returnType = '';
if (hasResponseBodyType) { if (!ref.content) {
const [pType, isArray, isClass] = hasResponseBodyType as any; responseSchema[PROCESS_AS.EMPTY] = true;
let data = `Promise<${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`; return responseSchema;
returnType = data; }
} else { if (ref.content?.['application/json']) {
returnType = 'Promise<number'; const { schema } = ref.content['application/json'];
} responseSchema[PROCESS_AS.JSON] = schemaParamParser(schema, this.openapi);
const shouldValidate = bodyParam.filter(b => b.isClass); }
if (shouldValidate.length > 0) { if (ref.content?.['text/palin']) {
returnType += ' | string[]'; const {
} "x-error-class": xErrorClass,
// append Error to default type return; "x-error-code": xErrorCode,
returnType += ' | Error>'; } = ref.content['text/palin'];
if (xErrorClass) {
const schemaLink = xErrorClass.split('/').pop();
const schema = this.openapi.components.schemas[schemaLink!];
responseSchema[PROCESS_AS.TEXT] = {
schema: schemaParamParser(schema, this.openapi),
xErrorCode,
onlyText: false,
}
} else {
responseSchema[PROCESS_AS.TEXT] = { onlyText: true };
}
}
return responseSchema;
});
let returnTypes = new Set();
bodyParam.forEach((param) => {
if (param.isClass) {
returnTypes.add(BAD_REQUES_HELPER);
importEntities.push({ type: BAD_REQUES_HELPER, isClass: true });
}
})
responsesSchema.forEach((responseSchema) => {
if (responseSchema[PROCESS_AS.JSON]) {
const { type, isClass, isImport } = responseSchema[PROCESS_AS.JSON]!;
returnTypes.add(type);
if (isImport) {
importEntities.push({ type: type, isClass });
}
}
if (responseSchema[PROCESS_AS.TEXT]) {
const { onlyText, schema } = responseSchema[PROCESS_AS.TEXT]!;
if (onlyText) {
returnTypes.add('string');
} else {
const { type, isClass, isImport } = schema!;
returnTypes.add(type);
if (isImport) {
importEntities.push({ type, isClass });
}
}
}
if (responseSchema[PROCESS_AS.EMPTY]) {
returnTypes.add('number');
}
});
returnTypes.add('undefined');
const returnType = `Promise<${Array.from(returnTypes).join(' | ')}>`;
const fetcher = apiClass.addMethod({ const fetcher = apiClass.addMethod({
isAsync: true, isAsync: true,
@@ -211,23 +287,19 @@ class ApiGenerator {
fetcher.addParameters(params); fetcher.addParameters(params);
fetcher.setBodyText((w) => { fetcher.setBodyText((w) => {
// Add data to URLSearchParams if (contentType === 'application/json') {
if (contentType === 'text/plain') { const shouldValidate = bodyParam.filter(b => b.isClass);
bodyParam.forEach((b) => {
w.writeLine(`const params = String(${b.name});`);
});
} else {
if (shouldValidate.length > 0) { if (shouldValidate.length > 0) {
w.writeLine(`const haveError: string[] = [];`); w.writeLine(`const haveError: string[] = [];`);
shouldValidate.forEach((b) => { shouldValidate.forEach((b) => {
w.writeLine(`const ${b.name}Valid = new ${b.pType}(${b.name});`); w.writeLine(`haveError.push(...${b.name}.validate());`);
w.writeLine(`haveError.push(...${b.name}Valid.validate());`);
}); });
w.writeLine(`if (haveError.length > 0) {`); w.writeLine(`if (haveError.length > 0) {`);
w.writeLine(` return Promise.resolve(haveError);`) w.writeLine(` return Promise.resolve(new ${BAD_REQUES_HELPER}(haveError));`)
w.writeLine(`}`); w.writeLine(`}`);
} }
} }
// Switch return of fetch in case on queryParams // Switch return of fetch in case on queryParams
if (queryParams.length > 0) { if (queryParams.length > 0) {
w.writeLine('const queryParams = {'); w.writeLine('const queryParams = {');
@@ -243,37 +315,36 @@ class ApiGenerator {
w.writeLine(` method: '${method.toUpperCase()}',`); w.writeLine(` method: '${method.toUpperCase()}',`);
// add Fetch options // add Fetch options
if (contentType && contentType !== 'multipart/form-data') {
w.writeLine(' headers: {');
w.writeLine(` 'Content-Type': '${contentType}',`);
w.writeLine(' },');
}
if (contentType) { if (contentType) {
switch (contentType) { w.writeLine(` body: JSON.stringify(${bodyParam.map((b) => b.isClass ? `${b.name}.serialize()` : b.name).join(', ')}),`);
case 'text/plain': }
w.writeLine(' body: params,');
break; w.writeLine('}).then(async (res) => {');
default: responsesSchema.forEach((responseSchema) => {
w.writeLine(` body: JSON.stringify(${bodyParam.map((b) => b.isClass ? `${b.name}Valid.serialize()` : b.name).join(', ')}),`); const { code } = responseSchema;
break; w.writeLine(` if (res.status === ${code}) {`);
if (responseSchema[PROCESS_AS.EMPTY]) {
w.writeLine(' return res.status;');
} }
} if (responseSchema[PROCESS_AS.TEXT]?.onlyText) {
w.writeLine(' return res.text();')
// Handle response }
if (hasResponseBodyType) { if (responseSchema[PROCESS_AS.JSON] && responseSchema[PROCESS_AS.TEXT]) {
w.writeLine('}).then(async (res) => {'); const { type } = responseSchema[PROCESS_AS.JSON]!;
w.writeLine(' if (res.status === 200) {'); const { schema, xErrorCode } = responseSchema[PROCESS_AS.TEXT]!;
w.writeLine(' return res.json();'); const { type: errType } = schema!;
} else { w.writeLine(' try {');
w.writeLine('}).then(async (res) => {'); w.writeLine(` return new ${type}(await res.json());`);
w.writeLine(' if (res.status === 200) {'); w.writeLine(' } catch {');
w.writeLine(' return res.status;'); w.writeLine(` return new ${errType}({ msg: await res.text() code: ${xErrorCode}} as any);`);
} w.writeLine(' }');
}
// Handle Error if (responseSchema[PROCESS_AS.JSON]) {
w.writeLine(' } else {'); const { type } = responseSchema[PROCESS_AS.JSON]!;
w.writeLine(' return new Error(String(res.status));'); w.writeLine(` return new ${type}(await res.json());`);
w.writeLine(' }'); }
w.writeLine(` }`);
})
w.writeLine('})'); w.writeLine('})');
}); });
}); });
@@ -288,17 +359,16 @@ class ApiGenerator {
} }
}); });
imports.sort((a,b) => a.type > b.type ? 1 : -1).forEach((ie) => { imports.sort((a,b) => a.type > b.type ? 1 : -1).forEach((ie) => {
const { type: pType, isClass } = ie; const { type: type, isClass } = ie;
if (isClass) { if (isClass) {
apiFile.addImportDeclaration({ apiFile.addImportDeclaration({
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`, moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${type}`,
defaultImport: pType, defaultImport: type,
namedImports: [`I${pType}`],
}); });
} else { } else {
apiFile.addImportDeclaration({ apiFile.addImportDeclaration({
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`, moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${type}`,
namedImports: [pType], namedImports: [type],
}); });
} }
}); });

View File

@@ -3,8 +3,8 @@ import * as path from 'path';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import * as morph from 'ts-morph'; import * as morph from 'ts-morph';
import { ENT_DIR } from '../../consts'; import { ENT_DIR, BAD_REQUES_HELPER } from '../../consts';
import { TYPES, toCamel, schemaParamParser, uncapitalize } from './utils'; import { TYPES, toCamel, schemaParamParser, capitalize, OpenApi, Schema } from './utils';
const { Project, QuoteKind } = morph; const { Project, QuoteKind } = morph;
@@ -17,7 +17,6 @@ if (!fs.existsSync(EntDir)) {
class EntitiesGenerator { class EntitiesGenerator {
project = new Project({ project = new Project({
tsConfigFilePath: './tsconfig.json', tsConfigFilePath: './tsconfig.json',
addFilesFromTsConfig: false,
manipulationSettings: { manipulationSettings: {
quoteKind: QuoteKind.Single, quoteKind: QuoteKind.Single,
usePrefixAndSuffixTextForRename: false, usePrefixAndSuffixTextForRename: false,
@@ -25,491 +24,480 @@ class EntitiesGenerator {
}, },
}); });
openapi: Record<string, any>; openapi: OpenApi;
schemas: Record<string, any>; schemas: Record<string, Schema>;
schemaNames: string[]; schemaNames: string[];
entities: morph.SourceFile[] = []; entities: morph.SourceFile[] = [];
constructor(openapi: Record<string, any>) { constructor(openapi: OpenApi) {
this.openapi = openapi; this.openapi = openapi;
this.schemas = openapi.components.schemas; this.schemas = openapi.components.schemas;
this.schemaNames = Object.keys(this.schemas); this.schemaNames = Object.keys(this.schemas);
this.generateEntities(); this.generateEntities();
this.generateUtils();
}
generateUtils = () => {
const helperFile = this.project.createSourceFile(`${EntDir}/${BAD_REQUES_HELPER}.ts`);
helperFile.addImportDeclaration({
moduleSpecifier: `./BadRequestResp`,
defaultImport: 'BadRequestResp',
});
helperFile.addImportDeclaration({
moduleSpecifier: `./ErrorCode`,
namedImports: ['ErrorCode'],
});
const helperClass = helperFile.addClass({
name: 'BadRequestHelper',
isDefaultExport: true,
extends: 'BadRequestResp',
properties: [{
type: 'string[]',
name: 'fields'
}]
});
const helperConstructor = helperClass.addConstructor({
parameters: [{
type: 'string[]',
name: 'fields'
}],
});
helperConstructor.setBodyText((w) => {
w.writeLine('super({ code: ErrorCode.JSN001, msg: \'Wrong fields value\' });');
w.writeLine('this.fields = fields;')
});
this.entities.push(helperFile);
} }
generateEntities = () => { generateEntities = () => {
this.schemaNames.forEach(this.generateEntity); this.schemaNames.forEach(this.generateEntity);
}; };
generateEntity = (sName: string) => { generateEntity = (schemaName: string) => {
const { properties, type, oneOf } = this.schemas[sName]; const { properties, type, oneOf, enum: en } = this.schemas[schemaName];
const notAClass = !properties && TYPES[type as keyof typeof TYPES]; const notAClass = !properties && TYPES[type as keyof typeof TYPES];
if (oneOf) { if (oneOf) {
this.generateOneOf(sName); this.generateOneOf(schemaName);
return; return;
} }
if (en) {
this.generateEnum(schemaName);
return;
}
if (notAClass) { if (notAClass) {
this.generateEnum(sName); this.generatePrimitive(schemaName)
} else { } else {
this.generateClass(sName); this.generateClass(schemaName);
} }
}; };
generateEnum = (sName: string) => { generatePrimitive = (schemaName: string) => {
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`); const entityFile = this.project.createSourceFile(`${EntDir}/${schemaName}.ts`);
entityFile.addStatements([ entityFile.addStatements([
'// This file was autogenerated. Please do not change.', '// This file was autogenerated. Please do not change.',
'// All changes will be overwrited on commit.',
'', '',
]); ]);
const { type: schemaType, description, pattern } = this.schemas[schemaName];
if (description) {
entityFile.addStatements(['\n/*', `Description: ${description}`, '*/\n']);
}
const { enum: enumMembers } = this.schemas[sName]; if (pattern) {
entityFile.addStatements(`const pattern = new RegExp('${pattern}')`);
}
const type: string = TYPES[schemaType as keyof typeof TYPES];
const entityClass = entityFile.addClass({
name: schemaName,
isDefaultExport: true,
extends: capitalize(type),
});
const ctor = entityClass.addConstructor({
parameters: [{
name: 'v',
type,
}],
});
ctor.setBodyText((w) => {
const { minLength, minimum, maxLength, maximum } = this.schemas[schemaName];
if (type === 'string') {
if (pattern) {
w.writeLine('if (!v.match(pattern)) {');
w.writeLine(' throw new Error();');
w.writeLine('}');
}
if (typeof minLength === 'number') {
w.writeLine(`if (v.length < ${minLength}) {`);
w.writeLine(' throw new Error();');
w.writeLine('}');
}
if (typeof maxLength === 'number') {
w.writeLine(`if (v.length > ${maxLength}) {`);
w.writeLine(' throw new Error();');
w.writeLine('}');
}
}
if (type === 'number') {
if (typeof minimum === 'number') {
w.writeLine(`if (v.length < ${minimum}) {`);
w.writeLine(' throw new Error();');
w.writeLine('}');
}
if (typeof maximum === 'number') {
w.writeLine(`if (v.length > ${maximum}) {`);
w.writeLine(' throw new Error();');
w.writeLine('}');
}
}
w.writeLine('super(v);');
});
this.entities.push(entityFile);
};
generateEnum = (schemaName: string) => {
const entityFile = this.project.createSourceFile(`${EntDir}/${schemaName}.ts`);
entityFile.addStatements([
'// This file was autogenerated. Please do not change.',
'',
]);
const { enum: enumMembers, description, example } = this.schemas[schemaName];
if (description) {
entityFile.addStatements(['\n/*', `Description: ${description}`, '*/\n']);
}
entityFile.addEnum({ entityFile.addEnum({
name: sName, name: schemaName,
members: enumMembers.map((e: string) => ({ name: e.toUpperCase(), value: e })), members: enumMembers!.map((e: string) => ({ name: e.toUpperCase(), value: e })),
isExported: true, isExported: true,
}); });
this.entities.push(entityFile); this.entities.push(entityFile);
}; };
generateOneOf = (sName: string) => { generateOneOf = (schemaName: string) => {
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`); const entityFile = this.project.createSourceFile(`${EntDir}/${schemaName}.ts`);
entityFile.addStatements([ entityFile.addStatements([
'// This file was autogenerated. Please do not change.', '// This file was autogenerated. Please do not change.',
'// All changes will be overwrited on commit.',
'', '',
]); ]);
const importEntities: { type: string, isClass: boolean }[] = []; const importEntities: { type: string, isClass: boolean }[] = [];
const entities = this.schemas[sName].oneOf.map((elem: any) => { const entities = this.schemas[schemaName].oneOf.map((elem: any) => {
const [ const {
pType, isArray, isClass, isImport, type: type, isArray, isClass, isImport,
] = schemaParamParser(elem, this.openapi); } = schemaParamParser(elem, this.openapi);
importEntities.push({ type: pType, isClass }); importEntities.push({ type: type, isClass });
return { type: pType, isArray }; return { type: type, isArray };
}); });
entityFile.addTypeAlias({ entityFile.addTypeAlias({
name: sName, name: schemaName,
isExported: true, isExported: true,
type: entities.map((e: any) => e.isArray ? `I${e.type}[]` : `I${e.type}`).join(' | '), type: entities.map((e: any) => e.isArray ? `I${e.type}[]` : `I${e.type}`).join(' | '),
}) })
// add import // add import
importEntities.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => { importEntities.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => {
const { type: pType, isClass } = ie; const { type: type, isClass } = ie;
if (isClass) { if (isClass) {
entityFile.addImportDeclaration({ entityFile.addImportDeclaration({
moduleSpecifier: `./${pType}`, moduleSpecifier: `./${type}`,
namedImports: [`I${pType}`], namedImports: [`I${type}`],
}); });
} else { } else {
entityFile.addImportDeclaration({ entityFile.addImportDeclaration({
moduleSpecifier: `./${pType}`, moduleSpecifier: `./${type}`,
namedImports: [pType], namedImports: [type],
}); });
} }
}); });
this.entities.push(entityFile); this.entities.push(entityFile);
} }
generateClass = (sName: string) => { generateClass = (schemaName: string) => {
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`); const entityFile = this.project.createSourceFile(`${EntDir}/${schemaName}.ts`);
entityFile.addStatements([ entityFile.addStatements([
'// This file was autogenerated. Please do not change.', '// This file was autogenerated. Please do not change.',
'// All changes will be overwrited on commit.',
'', '',
]); ]);
let { properties, required, allOf, $ref } = this.schemas[schemaName];
if (allOf) {
const refLink: string = allOf.find((obj: Record<string, any>) => obj.$ref).$ref;
let ref: any = refLink.split('/')
ref = ref.pop();
const reasign = allOf.find((obj: Record<string, any>) => !obj.$ref);
const newSchema: Schema = { ...this.schemas[ref], ...reasign };
properties = newSchema.properties;
required = newSchema.required;
}
const { properties: sProps, required, $ref, additionalProperties } = this.schemas[sName];
if ($ref) { if ($ref) {
const temp = $ref.split('/'); const refLink = $ref.split('/').pop()!;
const importSchemaName = `${temp[temp.length - 1]}`;
entityFile.addImportDeclaration({ entityFile.addImportDeclaration({
defaultImport: importSchemaName, defaultImport: refLink,
moduleSpecifier: `./${importSchemaName}`, moduleSpecifier: `./${refLink}`,
namedImports: [`I${importSchemaName}`], namedImports: [`I${refLink}`],
}); });
entityFile.addTypeAlias({ entityFile.addTypeAlias({
name: `I${sName}`, name: `I${schemaName}`,
type: `I${importSchemaName}`, type: `I${refLink}`,
isExported: true, isExported: true,
}) })
entityFile.addStatements(`export default ${importSchemaName};`); const entityClass = entityFile.addClass({
name: schemaName,
isDefaultExport: true,
extends: refLink,
})
const ctor = entityClass.addConstructor({
parameters: [{
name: 'props',
type: `I${schemaName}`,
}],
})
ctor.setBodyText((w) => {
w.writeLine('super(props);')
});
this.entities.push(entityFile); this.entities.push(entityFile);
return; return;
} }
const importEntities: { type: string, isClass: boolean }[] = [];
const entityInterface = entityFile.addInterface({ const entityInterface = entityFile.addInterface({
name: `I${sName}`, name: `I${schemaName}`,
isExported: true, isExported: true,
}); });
const sortedSProps = Object.keys(sProps || {}).sort(); const sortedProperties = Object.keys(properties || {}).sort();
const additionalPropsOnly = additionalProperties && sortedSProps.length === 0; let importEntities: { type: string, isClass: boolean }[] = [];
type SortedPropertiesTypesValues = ReturnType<typeof schemaParamParser> & {
computedType: string;
isRequired: boolean;
}
const sortedPropertiesTypes = sortedProperties.reduce((data, propertyName) => {
const isRequired = !!(required && required.includes(propertyName));
const parsed = schemaParamParser(properties![propertyName], this.openapi);
data[propertyName] = {
...parsed,
isRequired,
computedType: `${parsed.type}${parsed.isArray ? '[]' : ''}${isRequired ? '' : ' | undefined'}`
};
return data;
}, {} as Record<string, SortedPropertiesTypesValues>);
// add server response interface to entityFile // add server response interface to entityFile
sortedSProps.forEach((sPropName) => { sortedProperties.forEach((propertyName) => {
const [ const {
pType, isArray, isClass, isImport, isAdditional type, isArray, isClass, isImport
] = schemaParamParser(sProps[sPropName], this.openapi); } = sortedPropertiesTypes[propertyName];
if (isImport) { if (isImport) {
importEntities.push({ type: pType, isClass }); importEntities.push({ type: type, isClass });
} }
const propertyType = isAdditional
? `{ [key: string]: ${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''} }`
: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
entityInterface.addProperty({ entityInterface.addProperty({
name: sPropName, name: propertyName,
type: propertyType, type: `${isClass ? 'I' : ''}${type}${isArray ? '[]' : ''}`,
hasQuestionToken: !( hasQuestionToken: !(
(required && required.includes(sPropName)) || sProps[sPropName].required (required && required.includes(propertyName)) || properties![propertyName].required
), ),
}); });
}); });
if (additionalProperties) {
const [
pType, isArray, isClass, isImport, isAdditional
] = schemaParamParser(additionalProperties, this.openapi);
if (isImport) {
importEntities.push({ type: pType, isClass });
}
const type = isAdditional
? `{ [key: string]: ${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''} }`
: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
entityInterface.addIndexSignature({
keyName: 'key',
keyType: 'string',
returnType: additionalPropsOnly ? type : `${type} | undefined`,
});
}
// add import // add import
const imports: { type: string, isClass: boolean }[] = [];
const types: string[] = []; const types: string[] = [];
importEntities.forEach((i) => { importEntities = importEntities.filter((i) => {
const { type } = i; const { type } = i;
if (!types.includes(type)) { if (!types.includes(type)) {
imports.push(i);
types.push(type); types.push(type);
return true;
} }
return false;
}); });
imports.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => { importEntities.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => {
const { type: pType, isClass } = ie; const { type: type, isClass } = ie;
if (isClass) { if (isClass) {
entityFile.addImportDeclaration({ entityFile.addImportDeclaration({
defaultImport: pType, defaultImport: type,
moduleSpecifier: `./${pType}`, moduleSpecifier: `./${type}`,
namedImports: [`I${pType}`], namedImports: [`I${type}`],
}); });
} else { } else {
entityFile.addImportDeclaration({ entityFile.addImportDeclaration({
moduleSpecifier: `./${pType}`, moduleSpecifier: `./${type}`,
namedImports: [pType], namedImports: [type],
}); });
} }
}); });
const entityClass = entityFile.addClass({ const entityClass = entityFile.addClass({
name: sName, name: schemaName,
isDefaultExport: true, isDefaultExport: true,
}); });
// addProperties to class; // addProperties to class;
sortedSProps.forEach((sPropName) => { sortedProperties.forEach((propertyName) => {
const [pType, isArray, isClass, isImport, isAdditional] = schemaParamParser(sProps[sPropName], this.openapi); const { type, isArray, isClass, isEnum, isRequired, computedType } = sortedPropertiesTypes[propertyName];
const isRequred = (required && required.includes(sPropName))
|| sProps[sPropName].required;
const propertyType = isAdditional
? `{ [key: string]: ${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'} }`
: `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`;
entityClass.addProperty({ entityClass.addProperty({
name: `_${sPropName}`, name: `_${propertyName}`,
isReadonly: true, isReadonly: true,
type: propertyType, type: computedType,
}); });
const getter = entityClass.addGetAccessor({ const getter = entityClass.addGetAccessor({
name: toCamel(sPropName), name: toCamel(propertyName),
returnType: propertyType, returnType: computedType,
statements: [`return this._${sPropName};`], statements: [`return this._${propertyName};`],
}); });
const { description, example, minItems, maxItems, maxLength, minLength, maximum, minimum } = sProps[sPropName]; const { description, example, minItems, maxItems, maxLength, minLength, maximum, minimum } = properties![propertyName];
if (description || example) { if (description || example) {
getter.addJsDoc(`${example ? `Description: ${description}` : ''}${example ? `\nExample: ${example}` : ''}`); getter.addJsDoc(`${example ? `Description: ${description}` : ''}${example ? `\nExample: ${example}` : ''}`);
} }
if (minItems) { if (minItems) {
entityClass.addGetAccessor({ entityClass.addProperty({
isStatic: true, isStatic: true,
name: `${toCamel(sPropName)}MinItems`, isReadonly: true,
statements: [`return ${minItems};`], name: `${capitalize(toCamel(propertyName))}MinItems`,
initializer: `${minItems}`,
}); });
} }
if (maxItems) { if (maxItems) {
entityClass.addGetAccessor({ entityClass.addProperty({
isStatic: true, isStatic: true,
name: `${toCamel(sPropName)}MaxItems`, isReadonly: true,
statements: [`return ${maxItems};`], name: `${capitalize(toCamel(propertyName))}MaxItems`,
initializer: `${maxItems}`,
}); });
} }
if (typeof minLength === 'number') { if (typeof minLength === 'number') {
entityClass.addGetAccessor({ entityClass.addProperty({
isStatic: true, isStatic: true,
name: `${toCamel(sPropName)}MinLength`, isReadonly: true,
statements: [`return ${minLength};`], name: `${capitalize(toCamel(propertyName))}MinLength`,
initializer: `${minLength}`,
}); });
} }
if (maxLength) { if (maxLength) {
entityClass.addGetAccessor({ entityClass.addProperty({
isStatic: true, isStatic: true,
name: `${toCamel(sPropName)}MaxLength`, isReadonly: true,
statements: [`return ${maxLength};`], name: `${capitalize(toCamel(propertyName))}MaxLength`,
initializer: `${maxLength}`,
}); });
} }
if (typeof minimum === 'number') { if (typeof minimum === 'number') {
entityClass.addGetAccessor({ entityClass.addProperty({
isStatic: true, isStatic: true,
name: `${toCamel(sPropName)}MinValue`, isReadonly: true,
statements: [`return ${minimum};`], name: `${capitalize(toCamel(propertyName))}MinValue`,
initializer: `${minimum}`,
}); });
} }
if (maximum) { if (maximum) {
entityClass.addGetAccessor({ entityClass.addProperty({
isStatic: true, isStatic: true,
name: `${toCamel(sPropName)}MaxValue`, isReadonly: true,
statements: [`return ${maximum};`], name: `${capitalize(toCamel(propertyName))}MaxValue`,
initializer: `${maximum}`,
}); });
} }
if (!(isArray && isClass) && !isClass) {
const isEnum = !isClass && isImport;
const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required;
const { maxLength, minLength, maximum, minimum } = sProps[sPropName];
const haveValidationFields = maxLength || typeof minLength === 'number' || maximum || typeof minimum === 'number';
if (isRequired || haveValidationFields) {
const prop = toCamel(sPropName);
const validateField = entityClass.addMethod({
isStatic: true,
name: `${prop}Validate`,
returnType: `boolean`,
parameters: [{
name: prop,
type: `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`,
}],
})
validateField.setBodyText((w) => {
w.write('return ');
const nonRequiredCall = isRequired ? prop : `!${prop} ? true : ${prop}`;
if (pType === 'string') {
if (isArray) {
w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && (typeof p === 'string' && !!p.trim()), true)`);
} else {
if (typeof minLength === 'number' && maxLength) {
w.write(`(${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength})`);
}
if (typeof minLength !== 'number' || !maxLength) {
w.write(`${isRequired ? `typeof ${prop} === 'string'` : `!${prop} ? true : typeof ${prop} === 'string'`} && !!${nonRequiredCall}.trim()`);
}
}
} else if (pType === 'number') {
if (isArray) {
w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && typeof p === 'number', true)`);
} else {
if (typeof minimum === 'number' && maximum) {
w.write(`${isRequired ? `${prop} >= ${minimum} && ${prop} <= ${maximum}` : `!${prop} ? true : ((${prop} >= ${minimum}) && (${prop} <= ${maximum}))`}`);
}
if (typeof minimum !== 'number' || !maximum) {
w.write(`${isRequired ? `typeof ${prop} === 'number'` : `!${prop} ? true : typeof ${prop} === 'number'`}`);
}
}
} else if (pType === 'boolean') {
w.write(`${isRequired ? `typeof ${prop} === 'boolean'` : `!${prop} ? true : typeof ${prop} === 'boolean'`}`);
} else if (isEnum) {
if (isArray){
w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && Object.keys(${pType}).includes(${prop}), true)`);
} else {
w.write(`${isRequired ? `Object.keys(${pType}).includes(${prop})` : `!${prop} ? true : typeof ${prop} === 'boolean'`}`);
}
}
w.write(';');
});
}
}
}); });
if (additionalProperties) {
const [
pType, isArray, isClass, isImport, isAdditional
] = schemaParamParser(additionalProperties, this.openapi);
const type = `Record<string, ${pType}${isArray ? '[]' : ''}>`;
entityClass.addProperty({
name: additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`,
isReadonly: true,
type: type,
});
}
// add constructor; // add constructor;
const ctor = entityClass.addConstructor({ const ctor = entityClass.addConstructor({
parameters: [{ parameters: [{
name: 'props', name: 'props',
type: `I${sName}`, type: `I${schemaName}`,
}], }],
}); });
ctor.setBodyText((w) => { ctor.setBodyText((w) => {
if (additionalProperties) { sortedProperties.forEach((propertyName) => {
const [ const { type, isArray, isClass, isRequired } = sortedPropertiesTypes[propertyName];
pType, isArray, isClass, isImport, isAdditional
] = schemaParamParser(additionalProperties, this.openapi); const indent = !isRequired ? ' ' : '';
w.writeLine(`this.${additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`} = Object.entries(props).reduce<Record<string, ${pType}>>((prev, [key, value]) => {`); if (!isRequired) {
if (isClass) { if ((type === 'boolean' || type === 'number' || type ==='string') && !isClass && !isArray) {
w.writeLine(` prev[key] = new ${pType}(value!);`); w.writeLine(`if (typeof props.${propertyName} === '${type}') {`);
} else {
w.writeLine(`if (props.${propertyName}) {`);
}
}
if (isArray && isClass) {
w.writeLine(`${indent}this._${propertyName} = props.${propertyName}.map((p) => new ${type}(p));`);
} else if (isClass) {
w.writeLine(`${indent}this._${propertyName} = new ${type}(props.${propertyName});`);
} else { } else {
w.writeLine(' prev[key] = value!;') if (type === 'string' && !isArray) {
} w.writeLine(`${indent}this._${propertyName} = props.${propertyName}.trim();`);
w.writeLine(' return prev;');
w.writeLine('}, {})');
return;
}
sortedSProps.forEach((sPropName) => {
const [
pType, isArray, isClass, , isAdditional
] = schemaParamParser(sProps[sPropName], this.openapi);
const req = (required && required.includes(sPropName))
|| sProps[sPropName].required;
if (!req) {
if ((pType === 'boolean' || pType === 'number' || pType ==='string') && !isClass && !isArray) {
w.writeLine(`if (typeof props.${sPropName} === '${pType}') {`);
} else { } else {
w.writeLine(`if (props.${sPropName}) {`); w.writeLine(`${indent}this._${propertyName} = props.${propertyName};`);
} }
} }
if (isAdditional) { if (!isRequired) {
if (isArray && isClass) {
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => {
return { ...prev, [key]: new ${pType}(p[key])};
},{}))`);
} else if (isClass) {
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
return { ...prev, [key]: new ${pType}(props.${sPropName}[key])};
},{})`);
} else {
if (pType === 'string' && !isArray) {
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
return { ...prev, [key]: props.${sPropName}[key].trim()};
},{})`);
} else {
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
return { ...prev, [key]: props.${sPropName}[key]};
},{})`);
}
}
} else {
if (isArray && isClass) {
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.map((p) => new ${pType}(p));`);
} else if (isClass) {
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = new ${pType}(props.${sPropName});`);
} else {
if (pType === 'string' && !isArray) {
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.trim();`);
} else {
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName};`);
}
}
}
if (!req) {
w.writeLine('}'); w.writeLine('}');
} }
}); });
}); });
// add serialize method; // add serialize method;
const serialize = entityClass.addMethod({ const serialize = entityClass.addMethod({
isStatic: false, isStatic: false,
name: 'serialize', name: 'serialize',
returnType: `I${sName}`, returnType: `I${schemaName}`,
}); });
serialize.setBodyText((w) => { serialize.setBodyText((w) => {
if (additionalProperties) { w.writeLine(`const data: I${schemaName} = {`);
const [
pType, isArray, isClass, isImport, isAdditional
] = schemaParamParser(additionalProperties, this.openapi);
w.writeLine(`return Object.entries(this.${additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`}).reduce<Record<string, ${isClass ? 'I' : ''}${pType}>>((prev, [key, value]) => {`);
if (isClass) {
w.writeLine(` prev[key] = value.serialize();`);
} else {
w.writeLine(' prev[key] = value;')
}
w.writeLine(' return prev;');
w.writeLine('}, {})');
return;
}
w.writeLine(`const data: I${sName} = {`);
const unReqFields: string[] = []; const unReqFields: string[] = [];
sortedSProps.forEach((sPropName) => {
const req = (required && required.includes(sPropName)) sortedProperties.forEach((propertyName) => {
|| sProps[sPropName].required; const {isArray, isClass, isRequired } = sortedPropertiesTypes[propertyName];
const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi); if (!isRequired) {
if (!req) { unReqFields.push(propertyName);
unReqFields.push(sPropName);
return; return;
} }
if (isAdditional) { if (isArray && isClass) {
if (isArray && isClass) { w.writeLine(` ${propertyName}: this._${propertyName}.map((p) => p.serialize()),`);
w.writeLine(` ${sPropName}: this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }))),`); } else if (isClass) {
} else if (isClass) { w.writeLine(` ${propertyName}: this._${propertyName}.serialize(),`);
w.writeLine(` ${sPropName}: Object.keys(this._${sPropName}).reduce<Record<string, any>>((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {}),`);
} else {
w.writeLine(` ${sPropName}: Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] })),`);
}
} else { } else {
if (isArray && isClass) { w.writeLine(` ${propertyName}: this._${propertyName},`);
w.writeLine(` ${sPropName}: this._${sPropName}.map((p) => p.serialize()),`);
} else if (isClass) {
w.writeLine(` ${sPropName}: this._${sPropName}.serialize(),`);
} else {
w.writeLine(` ${sPropName}: this._${sPropName},`);
}
} }
}); });
w.writeLine('};'); w.writeLine('};');
unReqFields.forEach((sPropName) => {
const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi); unReqFields.forEach((propertyName) => {
w.writeLine(`if (typeof this._${sPropName} !== 'undefined') {`); const { isArray, isClass } = sortedPropertiesTypes[propertyName];
if (isAdditional) { w.writeLine(`if (typeof this._${propertyName} !== 'undefined') {`);
if (isArray && isClass) { if (isArray && isClass) {
w.writeLine(` data.${sPropName} = this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }), {}));`); w.writeLine(` data.${propertyName} = this._${propertyName}.map((p) => p.serialize());`);
} else if (isClass) { } else if (isClass) {
w.writeLine(` data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {});`); w.writeLine(` data.${propertyName} = this._${propertyName}.serialize();`);
} else {
w.writeLine(` data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] }), {});`);
}
} else { } else {
if (isArray && isClass) { w.writeLine(` data.${propertyName} = this._${propertyName};`);
w.writeLine(` data.${sPropName} = this._${sPropName}.map((p) => p.serialize());`);
} else if (isClass) {
w.writeLine(` data.${sPropName} = this._${sPropName}.serialize();`);
} else {
w.writeLine(` data.${sPropName} = this._${sPropName};`);
}
} }
w.writeLine(`}`); w.writeLine(`}`);
}); });
w.writeLine('return data;'); w.writeLine('return data;');
@@ -522,74 +510,55 @@ class EntitiesGenerator {
returnType: `string[]`, returnType: `string[]`,
}) })
validate.setBodyText((w) => { validate.setBodyText((w) => {
if (additionalPropsOnly) { w.writeLine('const validateRequired = {');
w.writeLine('return []') Object.keys(properties || {}).forEach((propertyName) => {
return; const { isArray, isClass, type, isRequired } = sortedPropertiesTypes[propertyName];
} const { maxLength, minLength, maximum, minimum } = properties![propertyName];
w.writeLine('const validate = {');
Object.keys(sProps || {}).forEach((sPropName) => {
const [pType, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
const { maxLength, minLength, maximum, minimum } = sProps[sPropName]; const nonRequiredCall = isRequired ? `this._${propertyName}` : `!this._${propertyName} ? true : this._${propertyName}`;
const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required;
const nonRequiredCall = isRequired ? `this._${sPropName}` : `!this._${sPropName} ? true : this._${sPropName}`;
if (isArray && isClass) { if (isArray && isClass) {
w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && p.validate().length === 0, true),`); w.writeLine(` ${propertyName}: ${nonRequiredCall}.reduce<boolean>((result, p) => result && p.validate().length === 0, true),`);
} else if (isClass && !isAdditional) { } else if (isClass) {
w.writeLine(` ${sPropName}: ${nonRequiredCall}.validate().length === 0,`); w.writeLine(` ${propertyName}: ${nonRequiredCall}.validate().length === 0,`);
} else { } else {
if (pType === 'string') { if (type === 'string') {
if (isArray) { if (isArray) {
w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && typeof p === 'string', true),`); w.writeLine(` ${propertyName}: ${nonRequiredCall}.reduce<boolean>((result, p) => result && typeof p === 'string', true),`);
} else { } else {
if (typeof minLength === 'number' && maxLength) { if (typeof minLength === 'number' && maxLength) {
w.writeLine(` ${sPropName}: (${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength}),`); w.writeLine(` ${propertyName}: (${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength}),`);
} }
if (typeof minLength !== 'number' || !maxLength) { if (typeof minLength !== 'number' || !maxLength) {
w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'string'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'string'`} && !this._${sPropName} ? true : this._${sPropName},`); w.writeLine(` ${propertyName}: ${isRequired ? `typeof this._${propertyName} === 'string' && !!this._${propertyName}.trim()` : `!this._${propertyName} ? true : typeof this._${propertyName} === 'string'`},`);
} }
} }
} else if (pType === 'number') { } else if (type === 'number') {
if (isArray) { if (isArray) {
w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && typeof p === 'number', true),`); w.writeLine(` ${propertyName}: ${nonRequiredCall}.reduce<boolean>((result, p) => result && typeof p === 'number', true),`);
} else { } else {
if (typeof minimum === 'number' && maximum) { if (typeof minimum === 'number' && maximum) {
w.writeLine(` ${sPropName}: ${isRequired ? `this._${sPropName} >= ${minimum} && this._${sPropName} <= ${maximum}` : `!this._${sPropName} ? true : ((this._${sPropName} >= ${minimum}) && (this._${sPropName} <= ${maximum}))`},`); w.writeLine(` ${propertyName}: ${isRequired ? `this._${propertyName} >= ${minimum} && this._${propertyName} <= ${maximum}` : `!this._${propertyName} ? true : ((this._${propertyName} >= ${minimum}) && (this._${propertyName} <= ${maximum}))`},`);
} }
if (typeof minimum !== 'number' || !maximum) { if (typeof minimum !== 'number' || !maximum) {
w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'number'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'number'`},`); w.writeLine(` ${propertyName}: ${isRequired ? `typeof this._${propertyName} === 'number'` : `!this._${propertyName} ? true : typeof this._${propertyName} === 'number'`},`);
} }
} }
} else if (pType === 'boolean') { } else if (type === 'boolean') {
w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'boolean'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'boolean'`},`); w.writeLine(` ${propertyName}: ${isRequired ? `typeof this._${propertyName} === 'boolean'` : `!this._${propertyName} ? true : typeof this._${propertyName} === 'boolean'`},`);
} }
} }
}); });
w.writeLine('};'); w.writeLine('};');
w.writeLine('const isError: string[] = [];') w.writeLine('const errorInFields: string[] = [];')
w.writeLine('Object.keys(validate).forEach((key) => {'); w.writeLine('Object.keys(validateRequired).forEach((key) => {');
w.writeLine(' if (!(validate as any)[key]) {'); w.writeLine(' if (!(validateRequired as any)[key]) {');
w.writeLine(' isError.push(key);'); w.writeLine(' errorInFields.push(key);');
w.writeLine(' }'); w.writeLine(' }');
w.writeLine('});'); w.writeLine('});');
w.writeLine('return isError;'); w.writeLine('return errorInFields;');
}); });
// add update method;
const update = entityClass.addMethod({
isStatic: false,
name: 'update',
returnType: `${sName}`,
});
update.addParameter({
name: 'props',
type: additionalPropsOnly ? `I${sName}` : `Partial<I${sName}>`,
});
update.setBodyText((w) => { w.writeLine(`return new ${sName}({ ...this.serialize(), ...props });`); });
this.entities.push(entityFile); this.entities.push(entityFile);
}; };

View File

@@ -19,65 +19,154 @@ const TYPES = {
boolean: 'boolean', boolean: 'boolean',
}; };
export enum SchemaType {
STRING = 'string',
OBJECT = 'object',
ARRAY = 'array',
BOOLEAN = 'boolean',
NUMBER = 'number',
INTEGER = 'integer',
}
export interface Schema {
allOf?: any[];
example?: string;
properties?: Record<string, Schema>;
required?: string[];
description?: string;
enum?: string[];
type: SchemaType;
pattern?: string;
oneOf?: any
items?: Schema;
additionalProperties?: Schema;
$ref?: string;
minItems?: number;
maxItems?: number;
maxLength?: number;
minLength?: number;
maximum?: number;
minimum?: number;
}
export interface Parameter {
description?: string;
example?: string;
in?: 'query' | 'body' | 'headers';
name: string;
schema: Schema;
required?: boolean;
}
export interface RequestBody {
content: {
'application/json'?: {
schema: Schema;
example?: string;
};
}
required?: boolean;
}
export interface Response {
content: {
'application/json'?: {
schema: Schema;
example?: string;
};
'text/palin'?: {
example?: string;
'x-error-class'?: string;
'x-error-code'?: string;
}
}
description?: string;
}
export interface Schemas {
parameters: Record<string, Parameter>;
requestBodies: Record<string, RequestBody>;
responses: Record<string, Response>;
schemas: Record<string, Schema>;
}
export interface OpenApi {
components: Schemas;
paths: any;
servers: {
description: string;
url: string;
}[]
}
/** /**
* @param schemaProp: valueof shema.properties[key] * @param schemaProp: valueof shema.properties[key]
* @param openApi: openapi object * @param openApi: openapi object
* @returns [propType - basicType or import one, isArray, isClass, isImport] * @returns [propType - basicType or import one, isArray, isClass, isImport]
*/ */
const schemaParamParser = (schemaProp: any, openApi: any): [string, boolean, boolean, boolean, boolean] => { interface SchemaParamParserReturn {
type: string;
isArray: boolean;
isClass: boolean;
isImport: boolean;
isAdditional: boolean;
isEnum: boolean;
}
const schemaParamParser = (schemaProp: Schema, openApi: OpenApi): SchemaParamParserReturn => {
let type = ''; let type = '';
let isImport = false; let isImport = false;
let isClass = false; let isClass = false;
let isArray = false; let isArray = false;
let isAdditional = false; let isAdditional = false;
let isEnum = false;
if (schemaProp.$ref || schemaProp.additionalProperties?.$ref) { if (schemaProp.$ref || schemaProp.additionalProperties?.$ref) {
const temp = (schemaProp.$ref || schemaProp.additionalProperties?.$ref).split('/'); type = (schemaProp.$ref || schemaProp.additionalProperties?.$ref)!.split('/').pop()!;
if (schemaProp.additionalProperties) { if (schemaProp.additionalProperties) {
isAdditional = true; isAdditional = true;
} }
const cl = openApi.components.schemas[type];
type = `${temp[temp.length - 1]}`;
if (cl.allOf) {
const cl = openApi ? openApi.components.schemas[type] : {}; const ref = cl.allOf.find((e) => !!e.$ref);
const link = schemaParamParser(ref, openApi);
return {...link, type};
}
if (cl.$ref) { if (cl.$ref) {
const link = schemaParamParser(cl, openApi); const link = schemaParamParser(cl, openApi);
link.shift(); return {...link, type};
return [type, ...link] as any;
} }
if (cl.type === 'string' && cl.enum) { if (cl.type === 'string' && cl.enum) {
isImport = true; isImport = true;
isEnum = true;
} }
if (cl.type === 'object' && !cl.oneOf) { if (cl.type === 'object' && !cl.oneOf) {
isClass = true; isClass = true;
isImport = true; isImport = true;
} else if (cl.type === 'array') { } else if (cl.type === 'array') {
const temp: any = schemaParamParser(cl.items, openApi); const temp = schemaParamParser(cl.items!, openApi);
type = `${temp[0]}`; type = temp.type;
isArray = true; isArray = true;
isClass = isClass || temp[2]; isClass = isClass || temp.isClass;
isImport = isImport || temp[3]; isImport = isImport || temp.isImport;
isEnum = isEnum || temp.isEnum;
} }
} else if (schemaProp.type === 'array') { } else if (schemaProp.type === 'array') {
const temp: any = schemaParamParser(schemaProp.items, openApi); const temp = schemaParamParser(schemaProp.items!, openApi);
type = `${temp[0]}`; type = temp.type
isArray = true; isArray = true;
isClass = isClass || temp[2]; isClass = isClass || temp.isClass;
isImport = isImport || temp[3]; isImport = isImport || temp.isImport;
isEnum = isEnum || temp.isEnum;
} else { } else {
type = (TYPES as Record<any, string>)[schemaProp.type]; type = (TYPES as Record<any, string>)[schemaProp.type];
} }
if (!type) {
// TODO: Fix bug with Error fields.
type = 'any';
// throw new Error('Failed to find entity type');
}
return [type, isArray, isClass, isImport, isAdditional]; return { type, isArray, isClass, isImport, isAdditional, isEnum };
}; };
export { TYPES, toCamel, capitalize, uncapitalize, schemaParamParser }; export { TYPES, toCamel, capitalize, uncapitalize, schemaParamParser };

View File

@@ -370,17 +370,16 @@
remark "^13.0.0" remark "^13.0.0"
unist-util-find-all-after "^3.0.2" unist-util-find-all-after "^3.0.2"
"@ts-morph/common@~0.6.0": "@ts-morph/common@~0.8.0":
version "0.6.0" version "0.8.0"
resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.6.0.tgz#cbd4ee57c5ef971511b9c5778e0bb8eb27de4783" resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.8.0.tgz#ae7b292df8258040465c50b378108ec8f09a9516"
integrity sha512-pI35nZz5bs3tL3btSVX2cWkAE8rc80F+Fn4TwSC6bQvn7fgn9IyLXVcAfpG6X6NBY5wN9TkSWXn/QYUkBvR/Fw== integrity sha512-YbjWiMXLMKxWxcMqP47nwZVWVBwoF5B65dtRz0lya2LetjldAPxTxRbRo1n4Iszr2tSvzXeaa+f1AbULmfc5uA==
dependencies: dependencies:
"@dsherret/to-absolute-glob" "^2.0.2" "@dsherret/to-absolute-glob" "^2.0.2"
fast-glob "^3.2.4" fast-glob "^3.2.5"
fs-extra "^9.0.1"
is-negated-glob "^1.0.0" is-negated-glob "^1.0.0"
multimatch "^4.0.0" mkdirp "^1.0.4"
typescript "~4.0.2" multimatch "^5.0.0"
"@types/anymatch@*": "@types/anymatch@*":
version "1.3.1" version "1.3.1"
@@ -1172,11 +1171,6 @@ async@^2.6.2:
dependencies: dependencies:
lodash "^4.17.14" lodash "^4.17.14"
at-least-node@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
atob@^2.1.2: atob@^2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
@@ -1593,7 +1587,7 @@ coa@^2.0.2:
chalk "^2.4.1" chalk "^2.4.1"
q "^1.1.2" q "^1.1.2"
code-block-writer@^10.1.0: code-block-writer@^10.1.1:
version "10.1.1" version "10.1.1"
resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-10.1.1.tgz#ad5684ed4bfb2b0783c8b131281ae84ee640a42f" resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-10.1.1.tgz#ad5684ed4bfb2b0783c8b131281ae84ee640a42f"
integrity sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw== integrity sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==
@@ -2910,9 +2904,9 @@ fastest-levenshtein@^1.0.12:
integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==
fastq@^1.6.0: fastq@^1.6.0:
version "1.10.0" version "1.11.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.10.0.tgz#74dbefccade964932cdf500473ef302719c652bb" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858"
integrity sha512-NL2Qc5L3iQEsyYzweq7qfgy5OtXCmGzGvhElGEd/SoFWEMOEczNh5s5ocaF01HDetxz+p8ecjNPA6cZxxIHmzA== integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==
dependencies: dependencies:
reusify "^1.0.4" reusify "^1.0.4"
@@ -3084,16 +3078,6 @@ fs-extra@^8.1.0:
jsonfile "^4.0.0" jsonfile "^4.0.0"
universalify "^0.1.0" universalify "^0.1.0"
fs-extra@^9.0.1:
version "9.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
dependencies:
at-least-node "^1.0.0"
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^2.0.0"
fs-minipass@^2.0.0: fs-minipass@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
@@ -3267,11 +3251,16 @@ gonzales-pe@^4.3.0:
dependencies: dependencies:
minimist "^1.2.5" minimist "^1.2.5"
graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.2.4:
version "4.2.4" version "4.2.4"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.6"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
handle-thing@^2.0.0: handle-thing@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
@@ -4145,15 +4134,6 @@ jsonfile@^4.0.0:
optionalDependencies: optionalDependencies:
graceful-fs "^4.1.6" graceful-fs "^4.1.6"
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
dependencies:
universalify "^2.0.0"
optionalDependencies:
graceful-fs "^4.1.6"
"jsx-ast-utils@^2.4.1 || ^3.0.0": "jsx-ast-utils@^2.4.1 || ^3.0.0":
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz#41108d2cec408c3453c1bbe8a4aae9e1e2bd8f82" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz#41108d2cec408c3453c1bbe8a4aae9e1e2bd8f82"
@@ -4784,10 +4764,10 @@ multicast-dns@^6.0.1:
dns-packet "^1.3.1" dns-packet "^1.3.1"
thunky "^1.0.2" thunky "^1.0.2"
multimatch@^4.0.0: multimatch@^5.0.0:
version "4.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-4.0.0.tgz#8c3c0f6e3e8449ada0af3dd29efb491a375191b3" resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6"
integrity sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ== integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==
dependencies: dependencies:
"@types/minimatch" "^3.0.3" "@types/minimatch" "^3.0.3"
array-differ "^3.0.0" array-differ "^3.0.0"
@@ -6292,6 +6272,11 @@ querystringify@^2.1.1:
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
queue-microtask@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.2.tgz#abf64491e6ecf0f38a6502403d4cda04f372dfd3"
integrity sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg==
quick-lru@^4.0.1: quick-lru@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
@@ -7086,9 +7071,11 @@ rimraf@^3.0.2:
glob "^7.1.3" glob "^7.1.3"
run-parallel@^1.1.9: run-parallel@^1.1.9:
version "1.1.10" version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.10.tgz#60a51b2ae836636c81377df16cb107351bcd13ef" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
integrity sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw== integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
dependencies:
queue-microtask "^1.2.2"
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2" version "5.1.2"
@@ -7942,14 +7929,14 @@ ts-loader@^8.0.6:
micromatch "^4.0.0" micromatch "^4.0.0"
semver "^7.3.4" semver "^7.3.4"
ts-morph@^8.1.2: ts-morph@^10.0.1:
version "8.2.0" version "10.0.1"
resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-8.2.0.tgz#41d83cd501cbd897eb029ac489d6d5b927555c57" resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-10.0.1.tgz#5a620cc4ef85e3e6d161989e690f44d0a0f723b0"
integrity sha512-NHHWu+7I2/AOZiTni5w3f+xCfIxrkzPCcQbTGa81Yk3pr23a4h9xLLEE6tIGuYIubWjkjr9QVC3ITqgmA5touQ== integrity sha512-T1zufImtp5goTLTFhzi7XuKR1y/f+Jwz1gSULzB045LFjXuoqVlR87sfkpyWow8u2JwgusCJrhOnwmHCFNutTQ==
dependencies: dependencies:
"@dsherret/to-absolute-glob" "^2.0.2" "@dsherret/to-absolute-glob" "^2.0.2"
"@ts-morph/common" "~0.6.0" "@ts-morph/common" "~0.8.0"
code-block-writer "^10.1.0" code-block-writer "^10.1.1"
ts-node@^9.0.0: ts-node@^9.0.0:
version "9.1.1" version "9.1.1"
@@ -8032,11 +8019,6 @@ typescript@^4.0.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
typescript@~4.0.2:
version "4.0.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389"
integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==
unc-path-regex@^0.1.2: unc-path-regex@^0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
@@ -8112,11 +8094,6 @@ universalify@^0.1.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
universalify@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
unpipe@1.0.0, unpipe@~1.0.0: unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"

2
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
go 1.14 go 1.14
require ( require (
github.com/AdguardTeam/dnsproxy v0.35.5 github.com/AdguardTeam/dnsproxy v0.33.9
github.com/AdguardTeam/golibs v0.4.4 github.com/AdguardTeam/golibs v0.4.4
github.com/AdguardTeam/urlfilter v0.14.3 github.com/AdguardTeam/urlfilter v0.14.3
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1

4
go.sum
View File

@@ -18,8 +18,8 @@ dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBr
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/AdguardTeam/dnsproxy v0.35.5 h1:SsRF0eDzuLGaSUDKABIu9Mn1joi4v4kvEU1vju2DQPQ= github.com/AdguardTeam/dnsproxy v0.33.9 h1:HUwywkhUV/M73E7qWcBAF+SdsNq742s82Lvox4pr/tM=
github.com/AdguardTeam/dnsproxy v0.35.5/go.mod h1:dkI9VWh43XlOzF2XogDm1EmoVl7PANOR4isQV6X9LZs= github.com/AdguardTeam/dnsproxy v0.33.9/go.mod h1:dkI9VWh43XlOzF2XogDm1EmoVl7PANOR4isQV6X9LZs=
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4= github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.4.2 h1:7M28oTZFoFwNmp8eGPb3ImmYbxGaJLyQXeIFVHjME0o= github.com/AdguardTeam/golibs v0.4.2 h1:7M28oTZFoFwNmp8eGPb3ImmYbxGaJLyQXeIFVHjME0o=
github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4= github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=

View File

@@ -7,7 +7,6 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"net" "net"
"strings"
"sync" "sync"
"time" "time"
@@ -464,16 +463,7 @@ func (s *v4Server) processRequest(req, resp *dhcpv4.DHCPv4) (*Lease, bool) {
} }
if lease.Expiry.Unix() != leaseExpireStatic { if lease.Expiry.Unix() != leaseExpireStatic {
// The trimming is required since some devices include trailing lease.Hostname = string(hostname)
// zero-byte in DHCP option length calculation.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2582.
//
// TODO(e.burkov): Remove after the trimming for hostname option
// will be added into github.com/insomniacslk/dhcp module.
hostnameStr := strings.TrimRight(string(hostname), "\x00")
lease.Hostname = hostnameStr
s.commitLease(lease) s.commitLease(lease)
} else if len(lease.Hostname) != 0 { } else if len(lease.Hostname) != 0 {
o := &optFQDN{ o := &optFQDN{

View File

@@ -276,24 +276,14 @@ func (s *Server) prepareUpstreamSettings() error {
upstreams = s.conf.UpstreamDNS upstreams = s.conf.UpstreamDNS
} }
upstreams = filterOutComments(upstreams) upstreams = filterOutComments(upstreams)
upstreamConfig, err := proxy.ParseUpstreamsConfig(upstreams, upstreamConfig, err := proxy.ParseUpstreamsConfig(upstreams, s.conf.BootstrapDNS, DefaultTimeout)
upstream.Options{
Bootstrap: s.conf.BootstrapDNS,
Timeout: DefaultTimeout,
},
)
if err != nil { if err != nil {
return fmt.Errorf("dns: proxy.ParseUpstreamsConfig: %w", err) return fmt.Errorf("dns: proxy.ParseUpstreamsConfig: %w", err)
} }
if len(upstreamConfig.Upstreams) == 0 { if len(upstreamConfig.Upstreams) == 0 {
log.Info("warning: no default upstream servers specified, using %v", defaultDNS) log.Info("warning: no default upstream servers specified, using %v", defaultDNS)
uc, err := proxy.ParseUpstreamsConfig(defaultDNS, uc, err := proxy.ParseUpstreamsConfig(defaultDNS, s.conf.BootstrapDNS, DefaultTimeout)
upstream.Options{
Bootstrap: s.conf.BootstrapDNS,
Timeout: DefaultTimeout,
},
)
if err != nil { if err != nil {
return fmt.Errorf("dns: failed to parse default upstreams: %v", err) return fmt.Errorf("dns: failed to parse default upstreams: %v", err)
} }

View File

@@ -150,7 +150,7 @@ func (req *dnsConfig) checkBootstrap() (string, error) {
return boot, fmt.Errorf("invalid bootstrap server address: empty") return boot, fmt.Errorf("invalid bootstrap server address: empty")
} }
if _, err := upstream.NewResolver(boot, upstream.Options{Timeout: 0}); err != nil { if _, err := upstream.NewResolver(boot, 0); err != nil {
return boot, fmt.Errorf("invalid bootstrap server address: %w", err) return boot, fmt.Errorf("invalid bootstrap server address: %w", err)
} }
} }
@@ -315,12 +315,7 @@ func ValidateUpstreams(upstreams []string) error {
return nil return nil
} }
_, err := proxy.ParseUpstreamsConfig(upstreams, _, err := proxy.ParseUpstreamsConfig(upstreams, []string{}, DefaultTimeout)
upstream.Options{
Bootstrap: []string{},
Timeout: DefaultTimeout,
},
)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -2,10 +2,13 @@ package home
import ( import (
"crypto/rand" "crypto/rand"
"crypto/sha256"
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math"
"math/big"
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
@@ -17,12 +20,8 @@ import (
) )
const ( const (
// cookieTTL is given in hours. cookieTTL = 365 * 24 // in hours
cookieTTL = 365 * 24
sessionCookieName = "agh_session" sessionCookieName = "agh_session"
// sessionTokenSize is the length of session token in bytes.
sessionTokenSize = 16
) )
type session struct { type session struct {
@@ -286,29 +285,16 @@ type loginJSON struct {
Password string `json:"password"` Password string `json:"password"`
} }
// newSessionToken returns cryptographically secure randomly generated slice of func getSession(u *User) ([]byte, error) {
// bytes of sessionTokenSize length. maxSalt := big.NewInt(math.MaxUint32)
// salt, err := rand.Int(rand.Reader, maxSalt)
// TODO(e.burkov): Think about using byte array instead of byte slice.
func newSessionToken() (data []byte, err error) {
randData := make([]byte, sessionTokenSize)
_, err = rand.Read(randData)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return randData, nil d := []byte(fmt.Sprintf("%s%s%s", salt, u.Name, u.PasswordHash))
} hash := sha256.Sum256(d)
return hash[:], nil
// cookieTimeFormat is the format to be used in (time.Time).Format for cookie's
// expiry field.
const cookieTimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
// cookieExpiryFormat returns the formatted exp to be used in cookie string.
// It's quite simple for now, but probably will be expanded in the future.
func cookieExpiryFormat(exp time.Time) (formatted string) {
return exp.Format(cookieTimeFormat)
} }
func (a *Auth) httpCookie(req loginJSON) (string, error) { func (a *Auth) httpCookie(req loginJSON) (string, error) {
@@ -317,23 +303,24 @@ func (a *Auth) httpCookie(req loginJSON) (string, error) {
return "", nil return "", nil
} }
sess, err := newSessionToken() sess, err := getSession(&u)
if err != nil { if err != nil {
return "", err return "", err
} }
now := time.Now().UTC() now := time.Now().UTC()
expire := now.Add(cookieTTL * time.Hour)
expstr := expire.Format(time.RFC1123)
expstr = expstr[:len(expstr)-len("UTC")] // "UTC" -> "GMT"
expstr += "GMT"
a.addSession(sess, &session{ s := session{}
userName: u.Name, s.userName = u.Name
expire: uint32(now.Unix()) + a.sessionTTL, s.expire = uint32(now.Unix()) + a.sessionTTL
}) a.addSession(sess, &s)
return fmt.Sprintf( return fmt.Sprintf("%s=%s; Path=/; HttpOnly; Expires=%s",
"%s=%s; Path=/; HttpOnly; Expires=%s", sessionCookieName, hex.EncodeToString(sess), expstr), nil
sessionCookieName, hex.EncodeToString(sess),
cookieExpiryFormat(now.Add(cookieTTL*time.Hour)),
), nil
} }
func handleLogin(w http.ResponseWriter, r *http.Request) { func handleLogin(w http.ResponseWriter, r *http.Request) {

View File

@@ -1,8 +1,6 @@
package home package home
import ( import (
"bytes"
"crypto/rand"
"encoding/hex" "encoding/hex"
"net/http" "net/http"
"net/url" "net/url"
@@ -13,7 +11,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@@ -27,34 +24,14 @@ func prepareTestDir() string {
return dir return dir
} }
func TestNewSessionToken(t *testing.T) {
// Successful case.
token, err := newSessionToken()
require.Nil(t, err)
assert.Len(t, token, sessionTokenSize)
// Break the rand.Reader.
prevReader := rand.Reader
t.Cleanup(func() {
rand.Reader = prevReader
})
rand.Reader = &bytes.Buffer{}
// Unsuccessful case.
token, err = newSessionToken()
require.NotNil(t, err)
assert.Empty(t, token)
}
func TestAuth(t *testing.T) { func TestAuth(t *testing.T) {
dir := prepareTestDir() dir := prepareTestDir()
t.Cleanup(func() { _ = os.RemoveAll(dir) }) defer func() { _ = os.RemoveAll(dir) }()
fn := filepath.Join(dir, "sessions.db") fn := filepath.Join(dir, "sessions.db")
users := []User{{ users := []User{
Name: "name", {Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2", }
}}
a := InitAuth(fn, nil, 60) a := InitAuth(fn, nil, 60)
s := session{} s := session{}
@@ -64,7 +41,7 @@ func TestAuth(t *testing.T) {
assert.Equal(t, checkSessionNotFound, a.checkSession("notfound")) assert.Equal(t, checkSessionNotFound, a.checkSession("notfound"))
a.RemoveSession("notfound") a.RemoveSession("notfound")
sess, err := newSessionToken() sess, err := getSession(&users[0])
assert.Nil(t, err) assert.Nil(t, err)
sessStr := hex.EncodeToString(sess) sessStr := hex.EncodeToString(sess)

View File

@@ -17,7 +17,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/util" "github.com/AdguardTeam/AdGuardHome/internal/util"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/utils" "github.com/AdguardTeam/golibs/utils"
) )
@@ -296,12 +295,7 @@ func (clients *clientsContainer) FindUpstreams(ip string) *proxy.UpstreamConfig
} }
if c.upstreamConfig == nil { if c.upstreamConfig == nil {
config, err := proxy.ParseUpstreamsConfig(c.Upstreams, config, err := proxy.ParseUpstreamsConfig(c.Upstreams, config.DNS.BootstrapDNS, dnsforward.DefaultTimeout)
upstream.Options{
Bootstrap: config.DNS.BootstrapDNS,
Timeout: dnsforward.DefaultTimeout,
},
)
if err == nil { if err == nil {
c.upstreamConfig = &config c.upstreamConfig = &config
} }

View File

@@ -146,7 +146,7 @@ func TestHome(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, http.StatusOK, resp.StatusCode)
// test DNS over UDP // test DNS over UDP
r, err := upstream.NewResolver("127.0.0.1:5354", upstream.Options{Timeout: 3 * time.Second}) r, err := upstream.NewResolver("127.0.0.1:5354", 3*time.Second)
assert.Nil(t, err) assert.Nil(t, err)
addrs, err := r.LookupIPAddr(context.TODO(), "static.adguard.com") addrs, err := r.LookupIPAddr(context.TODO(), "static.adguard.com")
assert.Nil(t, err) assert.Nil(t, err)

View File

@@ -21,7 +21,7 @@ type onChangedT func()
// AutoHosts - automatic DNS records // AutoHosts - automatic DNS records
type AutoHosts struct { type AutoHosts struct {
// lock protects table and tableReverse. // lock protects table and tableReverse.
lock sync.RWMutex lock sync.Mutex
// table is the host-to-IPs map. // table is the host-to-IPs map.
table map[string][]net.IP table map[string][]net.IP
// tableReverse is the IP-to-hosts map. // tableReverse is the IP-to-hosts map.
@@ -119,14 +119,15 @@ func (a *AutoHosts) Process(host string, qtype uint16) []net.IP {
} }
var ipsCopy []net.IP var ipsCopy []net.IP
a.lock.RLock() a.lock.Lock()
defer a.lock.RUnlock()
if ips, ok := a.table[host]; ok { if ips, ok := a.table[host]; ok {
ipsCopy = make([]net.IP, len(ips)) ipsCopy = make([]net.IP, len(ips))
copy(ipsCopy, ips) copy(ipsCopy, ips)
} }
a.lock.Unlock()
log.Debug("AutoHosts: answer: %s -> %v", host, ipsCopy) log.Debug("AutoHosts: answer: %s -> %v", host, ipsCopy)
return ipsCopy return ipsCopy
} }
@@ -144,8 +145,8 @@ func (a *AutoHosts) ProcessReverse(addr string, qtype uint16) (hosts []string) {
ipStr := ipReal.String() ipStr := ipReal.String()
a.lock.RLock() a.lock.Lock()
defer a.lock.RUnlock() defer a.lock.Unlock()
hosts = a.tableReverse[ipStr] hosts = a.tableReverse[ipStr]
@@ -160,8 +161,8 @@ func (a *AutoHosts) ProcessReverse(addr string, qtype uint16) (hosts []string) {
// List returns an IP-to-hostnames table. It is safe for concurrent use. // List returns an IP-to-hostnames table. It is safe for concurrent use.
func (a *AutoHosts) List() (ipToHosts map[string][]string) { func (a *AutoHosts) List() (ipToHosts map[string][]string) {
a.lock.RLock() a.lock.Lock()
defer a.lock.RUnlock() defer a.lock.Unlock()
ipToHosts = make(map[string][]string, len(a.tableReverse)) ipToHosts = make(map[string][]string, len(a.tableReverse))
for k, v := range a.tableReverse { for k, v := range a.tableReverse {
@@ -338,13 +339,10 @@ func (a *AutoHosts) updateHosts() {
} }
} }
func() { a.lock.Lock()
a.lock.Lock() a.table = table
defer a.lock.Unlock() a.tableReverse = tableRev
a.lock.Unlock()
a.table = table
a.tableReverse = tableRev
}()
a.notify() a.notify()
} }

View File

@@ -5,12 +5,10 @@
package util package util
import ( import (
"bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"runtime" "runtime"
"strings" "strings"
) )
@@ -66,43 +64,16 @@ func SplitNext(str *string, splitBy byte) string {
return strings.TrimSpace(s) return strings.TrimSpace(s)
} }
// IsOpenWRT returns true if host OS is OpenWRT. // IsOpenWRT checks if OS is OpenWRT.
func IsOpenWRT() bool { func IsOpenWRT() bool {
if runtime.GOOS != "linux" { if runtime.GOOS != "linux" {
return false return false
} }
const etcDir = "/etc" body, err := ioutil.ReadFile("/etc/os-release")
// TODO(e.burkov): Take care of dealing with fs package after updating
// Go version to 1.16.
fileInfos, err := ioutil.ReadDir(etcDir)
if err != nil { if err != nil {
return false return false
} }
// fNameSubstr is a part of a name of the desired file. return strings.Contains(string(body), "OpenWrt")
const fNameSubstr = "release"
osNameData := []byte("OpenWrt")
for _, fileInfo := range fileInfos {
if fileInfo.IsDir() {
continue
}
if !strings.Contains(fileInfo.Name(), fNameSubstr) {
continue
}
body, err := ioutil.ReadFile(filepath.Join(etcDir, fileInfo.Name()))
if err != nil {
continue
}
if bytes.Contains(body, osNameData) {
return true
}
}
return false
} }

View File

@@ -1279,8 +1279,6 @@
'type': 'string' 'type': 'string'
'edns_cs_enabled': 'edns_cs_enabled':
'type': 'boolean' 'type': 'boolean'
'disable_ipv6':
'type': 'boolean'
'dnssec_enabled': 'dnssec_enabled':
'type': 'boolean' 'type': 'boolean'
'cache_size': 'cache_size':

View File

@@ -11,10 +11,8 @@ set -e -f -u
readonly awk_program='/^v[0-9]+\.[0-9]+\.[0-9]+.*$/ { readonly awk_program='/^v[0-9]+\.[0-9]+\.[0-9]+.*$/ {
if (!$4) { if (!$4) {
# The last tag is a full release version, so bump the # The last tag is a full release version, so bump the
# minor release number and zero the patch release number # minor one to get the next one.
# to get the next release.
$2++; $2++;
$3 = 0;
} }
print($1 "." $2 "." $3); print($1 "." $2 "." $3);