В предыдущем посте я углубился в основы генераторов, проиллюстрировав их фундаментальное применение. Сегодня я хотел бы осветить практический сценарий из моего личного проекта. В частности, я покажу вам, как я провел рефакторинг части моего кода, где я использовал генератор.
Моя цель состояла в том, чтобы повысить функциональность свойств схемы JSON. Я стремился к бесшовной интеграции, при которой я мог бы легко получить доступ к свойствам схемы JSON, используя указатель JSON объекта. Кроме того, мне нужна была возможность получать свойство JSON исключительно на основе его указателя на схему. Для этого я создал индекс, который связывает свойства JSON с их соответствующими аналогами схемы, обеспечивая плавную навигацию между двумя объектами с помощью указателей JSON.
Изначально я набросал быстрое решение, которое, к сожалению, быстро запуталось:
import { JsonPointer } from "json-ptr";
import * as _ from "lodash";
const joinPath = (...args ) => args.filter(Boolean).join("/");
export function currentSchemaPath(k, v: any, parent: any, prefix: string) {
if (Array.isArray(v)) {
return joinPath(prefix, `${k}/items`);
}
if (typeof v === "object") {
if (Array.isArray(parent)) {
return joinPath(prefix, `properties`);
}
return joinPath(prefix, `${k}/properties`);
}
if (Array.isArray(parent)) {
return `${prefix}`;
}
return joinPath(prefix, `${k}`);
}
function currentPath(k, v: any, parent: any, prefix: string) {
return prefix ? joinPath(prefix, `${k}`) : `/${k}`;
}
function* nestedPairsSchemaPath(obj, prefix = null, pathPrefix = null) {
const keys = Object.keys(obj);
while (keys.length > 0) {
const k = keys.splice(0, 1)[0];
const type = typeof obj[k];
const jsonSchemaCurrentPath = prefix
? `${currentSchemaPath(k, obj[k], obj, prefix)}`
: `/properties/${currentSchemaPath(k, obj[k], obj, prefix)}`;
const path = currentPath(k, obj[k], obj, pathPrefix);
//currentSchemaPath returns properties object from parent, but if its object or array, we need to move 1 level up as
//it will have either /items or /properties as last element in path
let schemaPath = jsonSchemaCurrentPath;
if (Array.isArray(obj[k]) || typeof obj[k] === "object") {
schemaPath = JsonPointer.create(jsonSchemaCurrentPath)
.relative("1")
.toString();
}
if (!!obj[k] && type === "object") {
yield [schemaPath, path, obj[k], k, obj];
yield* nestedPairsSchemaPath(obj[k], jsonSchemaCurrentPath, path);
} else {
yield [schemaPath, path, obj[k], k, obj];
}
}
}
Чтобы приступить к процессу рефакторинга, я сначала установил небольшой набор защитных тестов. Это гарантирует, что основная функциональность останется неизменной во время изменений.
import { expect } from 'chai';
import { nestedPairsSchemaPath } from './nested-pairs-json-schema';
describe('nestedPairsSchemaPath', () => {
it('should handle a simple object', () => {
const input = { name: 'John' };
const result = Array.from(nestedPairsSchemaPath(input));
expect(result).to.deep.equal([
['/properties/name', '/name', 'John', 'name', input],
]);
});
it('should handle nested objects', () => {
const input = { user: { name: 'John', age: 30 } };
const result = Array.from(nestedPairsSchemaPath(input));
expect(result).to.deep.equal([
['/properties/user', '/user', { name: 'John', age: 30 }, 'user', input],
[
'/properties/user/properties/name',
'/user/name',
'John',
'name',
input.user,
],
['/properties/user/properties/age', '/user/age', 30, 'age', input.user],
]);
});
it('should handle arrays', () => {
const input = { names: ['John', 'Doe'] };
const result = Array.from(nestedPairsSchemaPath(input));
expect(result).to.deep.equal([
['/properties/names', '/names', ['John', 'Doe'], 'names', input],
['/properties/names/items', '/names/0', 'John', '0', input.names],
['/properties/names/items', '/names/1', 'Doe', '1', input.names],
]);
});
it('should handle an empty object', () => {
const input = {};
const result = Array.from(nestedPairsSchemaPath(input));
expect(result).to.deep.equal([]);
});
it('should handle null values', () => {
const input = { name: null };
const result = Array.from(nestedPairsSchemaPath(input));
expect(result).to.deep.equal([
['/properties/name', '/name', null, 'name', input],
]);
});
});
All tests have passed successfully.

Учитывая этот положительный результат, я решил инкапсулировать логику, связанную с определением путей JSON и схемы JSON, в выделенный вспомогательный класс.
import { JsonPointer } from 'json-ptr';
interface ObjectIteratorDescriptor {
k: any;
v: any;
parent: any;
jsonPathPrefix: string;
schemaPathPrefix: string;
}
const JSON_PATH_START = '/';
const JSON_SCHEMA_PATH_START = '/properties';
export class JsonPathResolver {
joinPath(...args: any[]): string {
//If first element is null, that means we
if (args[0] === null) {
args[0] = JSON_PATH_START;
}
return args.filter(Boolean).join('/');
}
joinSchemaPath(...args: any[]): string {
//If first element is null, that means we
if (args[0] === null) {
args[0] = JSON_SCHEMA_PATH_START;
}
return args.filter(Boolean).join('/');
}
/**
* If value is a non-primitive, resolve a path and return it
* If parent is an array, return a prefix path which results from aggregation during iteration
*/
getNonPrimitiveSchemaPath({
k,
v,
parent,
schemaPathPrefix,
}: ObjectIteratorDescriptor) {
if (Array.isArray(v)) {
return this.joinSchemaPath(schemaPathPrefix, `${k}/items`);
}
if (typeof v === 'object') {
if (Array.isArray(parent)) {
return this.joinSchemaPath(schemaPathPrefix, `properties`);
}
return this.joinSchemaPath(schemaPathPrefix, `${k}/properties`);
}
if (Array.isArray(parent)) {
return `${schemaPathPrefix}`;
}
}
/**
* Resolve current json schema path prefix for traversed element, to be passed to child elements
*/
currentSchemaPathPrefix(iteratorInfo: ObjectIteratorDescriptor) {
const { schemaPathPrefix, k } = iteratorInfo;
const nonPrimitivePath = this.getNonPrimitiveSchemaPath(iteratorInfo);
if (nonPrimitivePath) {
return nonPrimitivePath;
}
return this.joinSchemaPath(schemaPathPrefix, `${k}`);
}
/**
* Resolve current json schema path prefix for traversed element, to be passed to child elements
*/
currentSchemaPath(iteratorInfo: ObjectIteratorDescriptor) {
const { v } = iteratorInfo;
const schemaPathPrefix = this.currentSchemaPathPrefix(iteratorInfo);
if (Array.isArray(v) || typeof v === 'object') {
return JsonPointer.create(schemaPathPrefix).relative('1').toString();
}
return schemaPathPrefix;
}
/**
* Resolve current json path for traversed element
* @param k
* @param prefix
*/
currentJsonPath({ k, jsonPathPrefix }: ObjectIteratorDescriptor) {
return jsonPathPrefix ? this.joinPath(jsonPathPrefix, `${k}`) : `/${k}`;
}
}
Теперь приступим к рефакторингу генератора:
export function* nestedPairsSchemaPath(
obj,
schemaPathPrefix = null,
jsonPathPrefix = null,
) {
const keys = Object.keys(obj);
const pathResolver = new JsonPathResolver();
while (keys.length > 0) {
const k = keys.splice(0, 1)[0];
const descriptor = {
k,
v: obj[k],
parent: obj,
jsonPathPrefix,
schemaPathPrefix,
};
const schemaCurrentPath = pathResolver.currentSchemaPathPrefix(descriptor);
const jsonPath = pathResolver.currentJsonPath(descriptor);
// The `currentSchemaPath` method returns the parent's properties object.
// However, if the result is an object or an array, we must ascend one level.
// The prefix should end with either `/items` or `/properties`,
// but the desired schema path is actually a level above that.
const schemaPath = pathResolver.currentSchemaPath(descriptor);
yield [schemaPath, jsonPath, obj[k], k, obj];
if (!!obj[k] && typeof obj[k] === 'object') {
yield* nestedPairsSchemaPath(obj[k], schemaCurrentPath, jsonPath);
}
}
}
Быстрый взгляд на тесты снова после рефакторинга:

Визуально ничего не сломано, но чтобы убедиться, я провел все модульные и интеграционные тесты:

Всем зелени, больших успехов!

Давайте вернемся к нашей основной теме, здесь раскрывается суть:
yield [schemaPath, jsonPath, obj[k], k, obj];
if (!!obj[k] && typeof obj[k] === 'object') {
yield* nestedPairsSchemaPath(obj[k], schemaCurrentPath, jsonPath);
}
Для каждого элемента мы возвращаем schemaPath, jsonPath, значение, ключ и весь объект для последующей обработки. Если элемент оказывается объектом, мы делегируем управление для обработки всех его дочерних элементов, пока не пройдем весь объект.
Вот как я построил индекс, о котором упоминал ранее, используя генератор, который мы только что реорганизовали:
const pointerIndex = {};
for (const [schemaPath, path] of nestedPairsSchemaPath(docCopy)) {
const docPointer = JsonPointer.create(path);
const schemaPointer = JsonPointer.create(schemaPath);
docPointer.set(
pointerIndex,
{
docPointer,
schemaPointer,
},
true,
);
}
И вот оно! Исторически я много работал с генераторами, особенно для эмуляции реактивных потоков, подобных RxJS API. Если это вас заинтересовало, не стесняйтесь исследовать мою песочницу кода здесь. Оставайтесь с нами, так как я планирую вести хронику своего опыта и приключений по этой теме в одном из следующих постов. Спасибо за чтение, и до следующего раза!