В предыдущем посте я углубился в основы генераторов, проиллюстрировав их фундаментальное применение. Сегодня я хотел бы осветить практический сценарий из моего личного проекта. В частности, я покажу вам, как я провел рефакторинг части моего кода, где я использовал генератор.
Моя цель состояла в том, чтобы повысить функциональность свойств схемы 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. Если это вас заинтересовало, не стесняйтесь исследовать мою песочницу кода здесь. Оставайтесь с нами, так как я планирую вести хронику своего опыта и приключений по этой теме в одном из следующих постов. Спасибо за чтение, и до следующего раза!