diff --git a/package-lock.json b/package-lock.json index 517f2fb4..d4549553 100644 --- a/package-lock.json +++ b/package-lock.json @@ -720,6 +720,10 @@ "resolved": "packages/json-api-model", "link": true }, + "node_modules/@fresha/json-pointer": { + "resolved": "packages/json-pointer", + "link": true + }, "node_modules/@fresha/json-schema-codegen": { "resolved": "packages/json-schema-codegen", "link": true @@ -5991,6 +5995,24 @@ "resolved": "https://registry.npmjs.org/@fresha/api-tools-core/-/api-tools-core-0.1.0.tgz", "integrity": "sha512-ftB+NamXDT/gxDZJ4MshCianvufKZHAiSUdbVwHR+0hu52HpJ5WrXx8Ll5YtOZ+1eQ5lv4JcJv1gamjOHHAitg==" }, + "packages/json-pointer": { + "name": "@fresha/json-pointer", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@fresha/api-tools-core": "0.1.0", + "@fresha/eslint-config": "0.1.0", + "@fresha/jest-config": "0.1.0", + "@fresha/typescript-config": "0.1.0", + "typescript": "^4.7.2" + } + }, + "packages/json-pointer/node_modules/@fresha/api-tools-core": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@fresha/api-tools-core/-/api-tools-core-0.1.0.tgz", + "integrity": "sha512-ftB+NamXDT/gxDZJ4MshCianvufKZHAiSUdbVwHR+0hu52HpJ5WrXx8Ll5YtOZ+1eQ5lv4JcJv1gamjOHHAitg==", + "dev": true + }, "packages/json-schema-codegen": { "name": "@fresha/json-schema-codegen", "version": "0.1.0", @@ -7184,6 +7206,24 @@ } } }, + "@fresha/json-pointer": { + "version": "file:packages/json-pointer", + "requires": { + "@fresha/api-tools-core": "0.1.0", + "@fresha/eslint-config": "0.1.0", + "@fresha/jest-config": "0.1.0", + "@fresha/typescript-config": "0.1.0", + "typescript": "^4.7.2" + }, + "dependencies": { + "@fresha/api-tools-core": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@fresha/api-tools-core/-/api-tools-core-0.1.0.tgz", + "integrity": "sha512-ftB+NamXDT/gxDZJ4MshCianvufKZHAiSUdbVwHR+0hu52HpJ5WrXx8Ll5YtOZ+1eQ5lv4JcJv1gamjOHHAitg==", + "dev": true + } + } + }, "@fresha/json-schema-codegen": { "version": "file:packages/json-schema-codegen", "requires": { diff --git a/packages/json-pointer/.eslintrc b/packages/json-pointer/.eslintrc new file mode 100644 index 00000000..ff932a49 --- /dev/null +++ b/packages/json-pointer/.eslintrc @@ -0,0 +1,7 @@ +{ + "extends": ["@fresha"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": ["./tsconfig.eslint.json"] + } +} diff --git a/packages/json-pointer/LICENSE b/packages/json-pointer/LICENSE new file mode 100644 index 00000000..9386eb9d --- /dev/null +++ b/packages/json-pointer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Fresha + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/json-pointer/README.md b/packages/json-pointer/README.md new file mode 100644 index 00000000..36123ea7 --- /dev/null +++ b/packages/json-pointer/README.md @@ -0,0 +1,5 @@ +# @fresha/json-pointer + +This is an implementation of RFC-6901 JSON Pointer. + +## diff --git a/packages/json-pointer/jest.config.js b/packages/json-pointer/jest.config.js new file mode 100644 index 00000000..b13f4954 --- /dev/null +++ b/packages/json-pointer/jest.config.js @@ -0,0 +1 @@ +module.exports = require('@fresha/jest-config'); diff --git a/packages/json-pointer/package.json b/packages/json-pointer/package.json new file mode 100644 index 00000000..2898776a --- /dev/null +++ b/packages/json-pointer/package.json @@ -0,0 +1,55 @@ +{ + "name": "@fresha/json-pointer", + "version": "0.1.0", + "description": "Implementation of the JSON Pointer specification", + "main": "build/index.js", + "types": "build/index.d.ts", + "scripts": { + "build": "tsc", + "build:watch": "tsc --watch", + "check": "run-s lint test", + "check:fix": "run-s lint:fix test", + "clean": "rimraf ./build", + "eslint": "eslint ./src", + "eslint:fix": "eslint ./src --fix", + "lint": "run-s eslint typecheck", + "lint:fix": "run-s eslint:fix typecheck", + "prebuild": "npm run clean", + "prebuild:watch": "npm run clean", + "test": "jest", + "typecheck": "tsc --noEmit" + }, + "files": [ + "build/", + "package.json", + "LICENSE", + "README.md" + ], + "keywords": [ + "JSON", + "JSON pointer" + ], + "author": "Fresha Engineering", + "contributors": [ + { + "name": "Andriy Mykulyak", + "email": "andriy@fresha.com", + "url": "https://github.com/mykulyak" + } + ], + "maintainers": [ + { + "name": "Andriy Mykulyak", + "email": "andriy@fresha.com", + "url": "https://github.com/mykulyak" + } + ], + "license": "MIT", + "devDependencies": { + "@fresha/api-tools-core": "0.1.0", + "@fresha/eslint-config": "0.1.0", + "@fresha/jest-config": "0.1.0", + "@fresha/typescript-config": "0.1.0", + "typescript": "^4.7.2" + } +} diff --git a/packages/json-pointer/src/JSONPointer.test.ts b/packages/json-pointer/src/JSONPointer.test.ts new file mode 100644 index 00000000..20bbf6e9 --- /dev/null +++ b/packages/json-pointer/src/JSONPointer.test.ts @@ -0,0 +1,37 @@ +import { JSONPointer } from './JSONPointer'; + +import type { JSONValue } from '@fresha/api-tools-core'; + +test('constructor', () => { + expect(new JSONPointer('')).toBeInstanceOf(JSONPointer); + expect(new JSONPointer('/')).toBeInstanceOf(JSONPointer); + expect(new JSONPointer('/x/y/z')).toBeInstanceOf(JSONPointer); + expect(() => new JSONPointer('a/b/c')).toThrow(); +}); + +test('get', () => { + const testObj: JSONValue = { + numberProp: 1, + objProp: { + strProp: 'str', + boolProp: true, + }, + arrProp: [12, '34', { subProp: 'cdd' }], + nullProp: null, + }; + + expect(new JSONPointer('').get(testObj)).toBe(testObj); + expect(new JSONPointer('/numberProp').get(testObj)).toBe(1); + expect(new JSONPointer('/objProp/strProp').get(testObj)).toBe('str'); + expect(new JSONPointer('/arrProp/2/subProp').get(testObj)).toBe('cdd'); + expect(new JSONPointer('/nullProp').get(testObj)).toBe(null); + expect(new JSONPointer('/numberProp/nonExistend').get(testObj)).toBe(undefined); + expect(new JSONPointer('/objProp/nonExistent').get(testObj)).toBe(undefined); +}); + +test('toString', () => { + expect(new JSONPointer('').toString()).toBe(''); + expect(new JSONPointer('/prop/1/arr/obj/str/num/bool').toString()).toBe( + '/prop/1/arr/obj/str/num/bool', + ); +}); diff --git a/packages/json-pointer/src/JSONPointer.ts b/packages/json-pointer/src/JSONPointer.ts new file mode 100644 index 00000000..c48894a3 --- /dev/null +++ b/packages/json-pointer/src/JSONPointer.ts @@ -0,0 +1,32 @@ +import assert from 'assert'; + +import type { JSONObject, JSONValue } from '@fresha/api-tools-core'; + +export class JSONPointer { + private readonly segments: string[]; + + constructor(segments: string | string[]) { + this.segments = typeof segments === 'string' ? segments.split('/') : segments; + assert.equal(this.segments[0], ''); + } + + get(obj: JSONValue): JSONValue | undefined { + if (this.segments.length === 1) { + return obj; + } + let res: JSONValue | undefined = obj; + for (const seg of this.segments.slice(1)) { + if (seg) { + res = res != null ? (res as JSONObject)[seg] : undefined; + if (res === undefined) { + break; + } + } + } + return res; + } + + toString(): string { + return this.segments.join('/'); + } +} diff --git a/packages/json-pointer/src/JSONPointerResolver.test.ts b/packages/json-pointer/src/JSONPointerResolver.test.ts new file mode 100644 index 00000000..dd6019a1 --- /dev/null +++ b/packages/json-pointer/src/JSONPointerResolver.test.ts @@ -0,0 +1,17 @@ +import { JSONPointerResolver } from './JSONPointerResolver'; + +test('get', () => { + const testObj = { + numberProp: 1, + objProp: { + strProp: 'str', + boolProp: true, + }, + arrProp: [12, '34', { subProp: 'cdd' }], + nullProp: null, + }; + const resolver = new JSONPointerResolver(testObj); + + expect(resolver.get('/')).toBe(testObj); + expect(resolver.get('/objProp/strProp')).toBe('str'); +}); diff --git a/packages/json-pointer/src/JSONPointerResolver.ts b/packages/json-pointer/src/JSONPointerResolver.ts new file mode 100644 index 00000000..8e8e7ba9 --- /dev/null +++ b/packages/json-pointer/src/JSONPointerResolver.ts @@ -0,0 +1,16 @@ +import { JSONPointer } from './JSONPointer'; + +import type { JSONValue } from '@fresha/api-tools-core'; + +export class JSONPointerResolver { + private readonly obj: JSONValue; + + constructor(obj: JSONValue) { + this.obj = obj; + } + + get(ptrOrStr: string | string[] | JSONPointer): JSONValue | undefined { + const ptr = ptrOrStr instanceof JSONPointer ? ptrOrStr : new JSONPointer(ptrOrStr); + return ptr.get(this.obj); + } +} diff --git a/packages/json-pointer/src/index.test.ts b/packages/json-pointer/src/index.test.ts new file mode 100644 index 00000000..246f716d --- /dev/null +++ b/packages/json-pointer/src/index.test.ts @@ -0,0 +1,6 @@ +import { get } from './index'; + +test('get', () => { + expect(() => get({}, 'a/b')).toThrow(); + expect(get({ arr: [1, 2, 3] }, '/arr/1')).toBe(2); +}); diff --git a/packages/json-pointer/src/index.ts b/packages/json-pointer/src/index.ts new file mode 100644 index 00000000..6f3cfe0e --- /dev/null +++ b/packages/json-pointer/src/index.ts @@ -0,0 +1,14 @@ +import { JSONPointer } from './JSONPointer'; + +import type { JSONValue } from '@fresha/api-tools-core'; + +export { JSONPointer }; +export { JSONPointerResolver } from './JSONPointerResolver'; + +export const get = ( + obj: JSONValue, + ptrOrStr: string | string[] | JSONPointer, +): JSONValue | undefined => { + const ptr = ptrOrStr instanceof JSONPointer ? ptrOrStr : new JSONPointer(ptrOrStr); + return ptr.get(obj); +}; diff --git a/packages/json-pointer/tsconfig.eslint.json b/packages/json-pointer/tsconfig.eslint.json new file mode 100644 index 00000000..a8d4317b --- /dev/null +++ b/packages/json-pointer/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": [] +} diff --git a/packages/json-pointer/tsconfig.json b/packages/json-pointer/tsconfig.json new file mode 100644 index 00000000..1e2dc77b --- /dev/null +++ b/packages/json-pointer/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@fresha/typescript-config", + "compilerOptions": { + "outDir": "./build", + "rootDir": "./src" + }, + "include": [ + "./src/**/*.ts" + ], + "exclude": [ + "**/*.test.ts" + ] +} \ No newline at end of file